// Copyright 2013 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. // On Mac, one can't make shortcuts with command-line arguments. Instead, we // produce small app bundles which locate the Chromium framework and load it, // passing the appropriate data. This is the entry point into the framework for // those app bundles. #import #include #include "base/at_exit.h" #include "base/command_line.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/logging.h" #include "base/mac/bundle_locations.h" #include "base/mac/foundation_util.h" #include "base/mac/launch_services_util.h" #include "base/mac/mac_logging.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_nsautorelease_pool.h" #include "base/mac/scoped_nsobject.h" #include "base/mac/sdk_forward_declarations.h" #include "base/message_loop/message_loop.h" #include "base/strings/string_number_conversions.h" #include "base/strings/sys_string_conversions.h" #include "base/threading/thread.h" #include "chrome/common/chrome_constants.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/mac/app_mode_common.h" #include "chrome/common/mac/app_shim_messages.h" #include "chrome/grit/generated_resources.h" #include "ipc/ipc_channel_proxy.h" #include "ipc/ipc_listener.h" #include "ipc/ipc_message.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" namespace { // Timeout in seconds to wait for a reply for the initial Apple Event. Note that // kAEDefaultTimeout on Mac is "about one minute" according to Apple's // documentation, but is no longer supported for asynchronous Apple Events. const int kPingChromeTimeoutSeconds = 60; const app_mode::ChromeAppModeInfo* g_info; base::Thread* g_io_thread = NULL; } // namespace class AppShimController; // An application delegate to catch user interactions and send the appropriate // IPC messages to Chrome. @interface AppShimDelegate : NSObject { @private AppShimController* appShimController_; // Weak, initially NULL. BOOL terminateNow_; BOOL terminateRequested_; std::vector filesToOpenAtStartup_; } // The controller is initially NULL. Setting it indicates to the delegate that // the controller has finished initialization. - (void)setController:(AppShimController*)controller; // Gets files that were queued because the controller was not ready. // Returns whether any FilePaths were added to |out|. - (BOOL)getFilesToOpenAtStartup:(std::vector*)out; // If the controller is ready, this sends a FocusApp with the files to open. // Otherwise, this adds the files to |filesToOpenAtStartup_|. // Takes an array of NSString*. - (void)openFiles:(NSArray*)filename; // Terminate immediately. This is necessary as we override terminate: to send // a QuitApp message. - (void)terminateNow; @end // The AppShimController is responsible for communication with the main Chrome // process, and generally controls the lifetime of the app shim process. class AppShimController : public IPC::Listener { public: AppShimController(); ~AppShimController() override; // Called when the main Chrome process responds to the Apple Event ping that // was sent, or when the ping fails (if |success| is false). void OnPingChromeReply(bool success); // Called |kPingChromeTimeoutSeconds| after startup, to allow a timeout on the // ping event to be detected. void OnPingChromeTimeout(); // Connects to Chrome and sends a LaunchApp message. void Init(); // Create a channel from |socket_path| and send a LaunchApp message. void CreateChannelAndSendLaunchApp(const base::FilePath& socket_path); // Builds main menu bar items. void SetUpMenu(); void SendSetAppHidden(bool hidden); void SendQuitApp(); // Called when the app is activated, e.g. by clicking on it in the dock, by // dropping a file on the dock icon, or by Cmd+Tabbing to it. // Returns whether the message was sent. bool SendFocusApp(apps::AppShimFocusType focus_type, const std::vector& files); private: // IPC::Listener implemetation. bool OnMessageReceived(const IPC::Message& message) override; void OnChannelError() override; // If Chrome failed to launch the app, |success| will be false and the app // shim process should die. void OnLaunchAppDone(apps::AppShimLaunchResult result); // Hide this app. void OnHide(); // Requests user attention. void OnRequestUserAttention(); void OnSetUserAttention(apps::AppShimAttentionType attention_type); // Terminates the app shim process. void Close(); base::FilePath user_data_dir_; scoped_ptr channel_; base::scoped_nsobject delegate_; bool launch_app_done_; bool ping_chrome_reply_received_; NSInteger attention_request_id_; DISALLOW_COPY_AND_ASSIGN(AppShimController); }; AppShimController::AppShimController() : delegate_([[AppShimDelegate alloc] init]), launch_app_done_(false), ping_chrome_reply_received_(false), attention_request_id_(0) { // Since AppShimController is created before the main message loop starts, // NSApp will not be set, so use sharedApplication. [[NSApplication sharedApplication] setDelegate:delegate_]; } AppShimController::~AppShimController() { // Un-set the delegate since NSApplication does not retain it. [[NSApplication sharedApplication] setDelegate:nil]; } void AppShimController::OnPingChromeReply(bool success) { ping_chrome_reply_received_ = true; if (!success) { [NSApp terminate:nil]; return; } Init(); } void AppShimController::OnPingChromeTimeout() { if (!ping_chrome_reply_received_) [NSApp terminate:nil]; } void AppShimController::Init() { DCHECK(g_io_thread); SetUpMenu(); // Chrome will relaunch shims when relaunching apps. if (base::mac::IsOSLionOrLater()) [NSApp disableRelaunchOnLogin]; // The user_data_dir for shims actually contains the app_data_path. // I.e. //Web Applications/_crx_extensionid/ user_data_dir_ = g_info->user_data_dir.DirName().DirName().DirName(); CHECK(!user_data_dir_.empty()); base::FilePath symlink_path = user_data_dir_.Append(app_mode::kAppShimSocketSymlinkName); base::FilePath socket_path; if (!base::ReadSymbolicLink(symlink_path, &socket_path)) { // The path in the user data dir is not a symlink, try connecting directly. CreateChannelAndSendLaunchApp(symlink_path); return; } app_mode::VerifySocketPermissions(socket_path); CreateChannelAndSendLaunchApp(socket_path); } void AppShimController::CreateChannelAndSendLaunchApp( const base::FilePath& socket_path) { IPC::ChannelHandle handle(socket_path.value()); channel_ = IPC::ChannelProxy::Create(handle, IPC::Channel::MODE_NAMED_CLIENT, this, g_io_thread->message_loop_proxy().get()); bool launched_by_chrome = base::CommandLine::ForCurrentProcess()->HasSwitch( app_mode::kLaunchedByChromeProcessId); apps::AppShimLaunchType launch_type = launched_by_chrome ? apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL; [delegate_ setController:this]; std::vector files; [delegate_ getFilesToOpenAtStartup:&files]; channel_->Send(new AppShimHostMsg_LaunchApp( g_info->profile_dir, g_info->app_mode_id, launch_type, files)); } void AppShimController::SetUpMenu() { NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name); // Create a main menu since [NSApp mainMenu] is nil. base::scoped_nsobject main_menu([[NSMenu alloc] initWithTitle:title]); // The title of the first item is replaced by OSX with the name of the app and // bold styling. Create a dummy item for this and make it hidden. NSMenuItem* dummy_item = [main_menu addItemWithTitle:title action:nil keyEquivalent:@""]; base::scoped_nsobject dummy_submenu( [[NSMenu alloc] initWithTitle:title]); [dummy_item setSubmenu:dummy_submenu]; [dummy_item setHidden:YES]; // Construct an unbolded app menu, to match how it appears in the Chrome menu // bar when the app is focused. NSMenuItem* item = [main_menu addItemWithTitle:title action:nil keyEquivalent:@""]; base::scoped_nsobject submenu([[NSMenu alloc] initWithTitle:title]); [item setSubmenu:submenu]; // Add a quit entry. NSString* quit_localized_string = l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name); [submenu addItemWithTitle:quit_localized_string action:@selector(terminate:) keyEquivalent:@"q"]; // Add File, Edit, and Window menus. These are just here to make the // transition smoother, i.e. from another application to the shim then to // Chrome. [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC) action:nil keyEquivalent:@""]; [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC) action:nil keyEquivalent:@""]; [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC) action:nil keyEquivalent:@""]; [NSApp setMainMenu:main_menu]; } void AppShimController::SendQuitApp() { channel_->Send(new AppShimHostMsg_QuitApp); } bool AppShimController::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(AppShimController, message) IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone) IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide) IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention) IPC_MESSAGE_HANDLER(AppShimMsg_SetUserAttention, OnSetUserAttention) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() return handled; } void AppShimController::OnChannelError() { Close(); } void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) { if (result != apps::APP_SHIM_LAUNCH_SUCCESS) { Close(); return; } std::vector files; if ([delegate_ getFilesToOpenAtStartup:&files]) SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files); launch_app_done_ = true; } void AppShimController::OnHide() { [NSApp hide:nil]; } void AppShimController::OnRequestUserAttention() { OnSetUserAttention(apps::APP_SHIM_ATTENTION_INFORMATIONAL); } void AppShimController::OnSetUserAttention( apps::AppShimAttentionType attention_type) { switch (attention_type) { case apps::APP_SHIM_ATTENTION_CANCEL: [NSApp cancelUserAttentionRequest:attention_request_id_]; attention_request_id_ = 0; break; case apps::APP_SHIM_ATTENTION_CRITICAL: attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest]; break; case apps::APP_SHIM_ATTENTION_INFORMATIONAL: attention_request_id_ = [NSApp requestUserAttention:NSInformationalRequest]; break; case apps::APP_SHIM_ATTENTION_NUM_TYPES: NOTREACHED(); } } void AppShimController::Close() { [delegate_ terminateNow]; } bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type, const std::vector& files) { if (launch_app_done_) { channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files)); return true; } return false; } void AppShimController::SendSetAppHidden(bool hidden) { channel_->Send(new AppShimHostMsg_SetAppHidden(hidden)); } @implementation AppShimDelegate - (BOOL)getFilesToOpenAtStartup:(std::vector*)out { if (filesToOpenAtStartup_.empty()) return NO; out->insert(out->end(), filesToOpenAtStartup_.begin(), filesToOpenAtStartup_.end()); filesToOpenAtStartup_.clear(); return YES; } - (void)setController:(AppShimController*)controller { appShimController_ = controller; } - (void)openFiles:(NSArray*)filenames { std::vector filePaths; for (NSString* filename in filenames) filePaths.push_back(base::mac::NSStringToFilePath(filename)); // If the AppShimController is ready, try to send a FocusApp. If that fails, // (e.g. if launching has not finished), enqueue the files. if (appShimController_ && appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, filePaths)) { return; } filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(), filePaths.begin(), filePaths.end()); } - (BOOL)application:(NSApplication*)app openFile:(NSString*)filename { [self openFiles:@[filename]]; return YES; } - (void)application:(NSApplication*)app openFiles:(NSArray*)filenames { [self openFiles:filenames]; [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess]; } - (BOOL)applicationOpenUntitledFile:(NSApplication*)app { if (appShimController_) { return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN, std::vector()); } return NO; } - (void)applicationWillBecomeActive:(NSNotification*)notification { if (appShimController_) { appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL, std::vector()); } } - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication*)sender { if (terminateNow_ || !appShimController_) return NSTerminateNow; appShimController_->SendQuitApp(); // Wait for the channel to close before terminating. terminateRequested_ = YES; return NSTerminateLater; } - (void)applicationWillHide:(NSNotification*)notification { if (appShimController_) appShimController_->SendSetAppHidden(true); } - (void)applicationWillUnhide:(NSNotification*)notification { if (appShimController_) appShimController_->SendSetAppHidden(false); } - (void)terminateNow { if (terminateRequested_) { [NSApp replyToApplicationShouldTerminate:NSTerminateNow]; return; } terminateNow_ = YES; [NSApp terminate:nil]; } @end //----------------------------------------------------------------------------- // A ReplyEventHandler is a helper class to send an Apple Event to a process // and call a callback when the reply returns. // // This is used to 'ping' the main Chrome process -- once Chrome has sent back // an Apple Event reply, it's guaranteed that it has opened the IPC channel // that the app shim will connect to. @interface ReplyEventHandler : NSObject { base::Callback onReply_; AEDesc replyEvent_; } // Sends an Apple Event to the process identified by |psn|, and calls |replyFn| // when the reply is received. Internally this creates a ReplyEventHandler, // which will delete itself once the reply event has been received. + (void)pingProcess:(const ProcessSerialNumber&)psn andCall:(base::Callback)replyFn; @end @interface ReplyEventHandler (PrivateMethods) // Initialise the reply event handler. Doesn't register any handlers until // |-pingProcess:| is called. |replyFn| is the function to be called when the // Apple Event reply arrives. - (id)initWithCallback:(base::Callback)replyFn; // Sends an Apple Event ping to the process identified by |psn| and registers // to listen for a reply. - (void)pingProcess:(const ProcessSerialNumber&)psn; // Called when a response is received from the target process for the ping sent // by |-pingProcess:|. - (void)message:(NSAppleEventDescriptor*)event withReply:(NSAppleEventDescriptor*)reply; // Calls |onReply_|, passing it |success| to specify whether the ping was // successful. - (void)closeWithSuccess:(bool)success; @end @implementation ReplyEventHandler + (void)pingProcess:(const ProcessSerialNumber&)psn andCall:(base::Callback)replyFn { // The object will release itself when the reply arrives, or possibly earlier // if an unrecoverable error occurs. ReplyEventHandler* handler = [[ReplyEventHandler alloc] initWithCallback:replyFn]; [handler pingProcess:psn]; } @end @implementation ReplyEventHandler (PrivateMethods) - (id)initWithCallback:(base::Callback)replyFn { if ((self = [super init])) { onReply_ = replyFn; } return self; } - (void)pingProcess:(const ProcessSerialNumber&)psn { // Register the reply listener. NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; [em setEventHandler:self andSelector:@selector(message:withReply:) forEventClass:'aevt' andEventID:'ansr']; // Craft the Apple Event to send. NSAppleEventDescriptor* target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&psn length:sizeof(psn)]; NSAppleEventDescriptor* initial_event = [NSAppleEventDescriptor appleEventWithEventClass:app_mode::kAEChromeAppClass eventID:app_mode::kAEChromeAppPing targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID]; // Note that AESendMessage effectively ignores kAEDefaultTimeout, because this // call does not pass kAEWantReceipt (which is deprecated and unsupported on // Mac). Instead, rely on OnPingChromeTimeout(). OSStatus status = AESendMessage( [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout); if (status != noErr) { OSSTATUS_LOG(ERROR, status) << "AESendMessage"; [self closeWithSuccess:false]; } } - (void)message:(NSAppleEventDescriptor*)event withReply:(NSAppleEventDescriptor*)reply { [self closeWithSuccess:true]; } - (void)closeWithSuccess:(bool)success { onReply_.Run(success); NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr']; [self release]; } @end //----------------------------------------------------------------------------- extern "C" { // |ChromeAppModeStart()| is the point of entry into the framework from the app // mode loader. __attribute__((visibility("default"))) int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info); } // extern "C" int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { base::CommandLine::Init(info->argc, info->argv); base::mac::ScopedNSAutoreleasePool scoped_pool; base::AtExitManager exit_manager; chrome::RegisterPathProvider(); if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) { RAW_LOG(ERROR, "App Mode Loader too old."); return 1; } if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) { RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut."); return 1; } g_info = info; // Set bundle paths. This loads the bundles. base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path); base::mac::SetOverrideFrameworkBundlePath( g_info->chrome_versioned_path.Append(chrome::kFrameworkName)); // Calculate the preferred locale used by Chrome. // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls // [base::mac::OuterBundle() preferredLocalizations] which gets localizations // from the bundle of the running app (i.e. it is equivalent to // [[NSBundle mainBundle] preferredLocalizations]) instead of the target // bundle. NSArray* preferred_languages = [NSLocale preferredLanguages]; NSArray* supported_languages = [base::mac::OuterBundle() localizations]; std::string preferred_localization; for (NSString* language in preferred_languages) { if ([supported_languages containsObject:language]) { preferred_localization = base::SysNSStringToUTF8(language); break; } } std::string locale = l10n_util::NormalizeLocale( l10n_util::GetApplicationLocale(preferred_localization)); // Load localized strings. ui::ResourceBundle::InitSharedInstanceWithLocale( locale, NULL, ui::ResourceBundle::DO_NOT_LOAD_COMMON_RESOURCES); // Launch the IO thread. base::Thread::Options io_thread_options; io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO; base::Thread *io_thread = new base::Thread("CrAppShimIO"); io_thread->StartWithOptions(io_thread_options); g_io_thread = io_thread; // Find already running instances of Chrome. pid_t pid = -1; std::string chrome_process_id = base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( app_mode::kLaunchedByChromeProcessId); if (!chrome_process_id.empty()) { if (!base::StringToInt(chrome_process_id, &pid)) LOG(FATAL) << "Invalid PID: " << chrome_process_id; } else { NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier]; NSArray* existing_chrome = [NSRunningApplication runningApplicationsWithBundleIdentifier:chrome_bundle_id]; if ([existing_chrome count] > 0) pid = [[existing_chrome objectAtIndex:0] processIdentifier]; } AppShimController controller; base::MessageLoopForUI main_message_loop; main_message_loop.set_thread_name("MainThread"); base::PlatformThread::SetName("CrAppShimMain"); // In tests, launching Chrome does nothing, and we won't get a ping response, // so just assume the socket exists. if (pid == -1 && !base::CommandLine::ForCurrentProcess()->HasSwitch( app_mode::kLaunchedForTest)) { // Launch Chrome if it isn't already running. ProcessSerialNumber psn; base::CommandLine command_line(base::CommandLine::NO_PROGRAM); command_line.AppendSwitch(switches::kSilentLaunch); // If the shim is the app launcher, pass --show-app-list when starting a new // Chrome process to inform startup codepaths and load the correct profile. if (info->app_mode_id == app_mode::kAppListModeId) { command_line.AppendSwitch(switches::kShowAppList); } else { command_line.AppendSwitchPath(switches::kProfileDirectory, info->profile_dir); } bool success = base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(), command_line, kLSLaunchDefaults, &psn); if (!success) return 1; base::Callback on_ping_chrome_reply = base::Bind(&AppShimController::OnPingChromeReply, base::Unretained(&controller)); // This code abuses the fact that Apple Events sent before the process is // fully initialized don't receive a reply until its run loop starts. Once // the reply is received, Chrome will have opened its IPC port, guaranteed. [ReplyEventHandler pingProcess:psn andCall:on_ping_chrome_reply]; main_message_loop.PostDelayedTask( FROM_HERE, base::Bind(&AppShimController::OnPingChromeTimeout, base::Unretained(&controller)), base::TimeDelta::FromSeconds(kPingChromeTimeoutSeconds)); } else { // Chrome already running. Proceed to init. This could still fail if Chrome // is still starting up or shutting down, but the process will exit quickly, // which is preferable to waiting for the Apple Event to timeout after one // minute. main_message_loop.PostTask( FROM_HERE, base::Bind(&AppShimController::Init, base::Unretained(&controller))); } main_message_loop.Run(); return 0; }