diff options
Diffstat (limited to 'chrome/app_shim/chrome_main_app_mode_mac.mm')
-rw-r--r-- | chrome/app_shim/chrome_main_app_mode_mac.mm | 693 |
1 files changed, 693 insertions, 0 deletions
diff --git a/chrome/app_shim/chrome_main_app_mode_mac.mm b/chrome/app_shim/chrome_main_app_mode_mac.mm new file mode 100644 index 0000000..e9cf194 --- /dev/null +++ b/chrome/app_shim/chrome_main_app_mode_mac.mm @@ -0,0 +1,693 @@ +// 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 <Cocoa/Cocoa.h> +#include <vector> + +#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/path_service.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<NSApplicationDelegate> { + @private + AppShimController* appShimController_; // Weak, initially NULL. + BOOL terminateNow_; + BOOL terminateRequested_; + std::vector<base::FilePath> 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<base::FilePath>*)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(); + virtual ~AppShimController(); + + // 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<base::FilePath>& files); + + private: + // IPC::Listener implemetation. + virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; + virtual 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<IPC::ChannelProxy> channel_; + base::scoped_nsobject<AppShimDelegate> 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. <user_data_dir>/<profile_dir>/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 = + 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<base::FilePath> 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<NSMenu> 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<NSMenu> 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<NSMenu> 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<base::FilePath> 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<base::FilePath>& 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<base::FilePath>*)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<base::FilePath> 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<base::FilePath>()); + } + + return NO; +} + +- (void)applicationWillBecomeActive:(NSNotification*)notification { + if (appShimController_) { + appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL, + std::vector<base::FilePath>()); + } +} + +- (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<void(bool)> 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<void(bool)>)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<void(bool)>)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<void(bool)>)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<void(bool)>)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) { + 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 = 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 && + !CommandLine::ForCurrentProcess()->HasSwitch( + app_mode::kLaunchedForTest)) { + // Launch Chrome if it isn't already running. + ProcessSerialNumber psn; + CommandLine command_line(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<void(bool)> 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; +} |