• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// On Mac, one can't make shortcuts with command-line arguments. Instead, we
6// produce small app bundles which locate the Chromium framework and load it,
7// passing the appropriate data. This is the entry point into the framework for
8// those app bundles.
9
10#import <Cocoa/Cocoa.h>
11#include <vector>
12
13#include "apps/app_shim/app_shim_messages.h"
14#include "base/at_exit.h"
15#include "base/command_line.h"
16#include "base/files/file_path.h"
17#include "base/file_util.h"
18#include "base/logging.h"
19#include "base/mac/bundle_locations.h"
20#include "base/mac/foundation_util.h"
21#include "base/mac/launch_services_util.h"
22#include "base/mac/mac_logging.h"
23#include "base/mac/mac_util.h"
24#include "base/mac/scoped_nsautorelease_pool.h"
25#include "base/mac/scoped_nsobject.h"
26#include "base/mac/sdk_forward_declarations.h"
27#include "base/message_loop/message_loop.h"
28#include "base/path_service.h"
29#include "base/strings/string_number_conversions.h"
30#include "base/strings/sys_string_conversions.h"
31#include "base/threading/thread.h"
32#include "chrome/common/chrome_constants.h"
33#include "chrome/common/chrome_paths.h"
34#include "chrome/common/chrome_switches.h"
35#include "chrome/common/mac/app_mode_common.h"
36#include "grit/generated_resources.h"
37#include "ipc/ipc_channel_proxy.h"
38#include "ipc/ipc_listener.h"
39#include "ipc/ipc_message.h"
40#include "ui/base/resource/resource_bundle.h"
41#include "ui/base/l10n/l10n_util.h"
42
43namespace {
44
45// Timeout in seconds to wait for a reply for the initial Apple Event. Note that
46// kAEDefaultTimeout on Mac is "about one minute" according to Apple's
47// documentation, but is no longer supported for asynchronous Apple Events.
48const int kPingChromeTimeoutSeconds = 60;
49
50const app_mode::ChromeAppModeInfo* g_info;
51base::Thread* g_io_thread = NULL;
52
53}  // namespace
54
55class AppShimController;
56
57// An application delegate to catch user interactions and send the appropriate
58// IPC messages to Chrome.
59@interface AppShimDelegate : NSObject<NSApplicationDelegate> {
60 @private
61  AppShimController* appShimController_;  // Weak, initially NULL.
62  BOOL terminateNow_;
63  BOOL terminateRequested_;
64  std::vector<base::FilePath> filesToOpenAtStartup_;
65}
66
67// The controller is initially NULL. Setting it indicates to the delegate that
68// the controller has finished initialization.
69- (void)setController:(AppShimController*)controller;
70
71// Gets files that were queued because the controller was not ready.
72// Returns whether any FilePaths were added to |out|.
73- (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out;
74
75// If the controller is ready, this sends a FocusApp with the files to open.
76// Otherwise, this adds the files to |filesToOpenAtStartup_|.
77// Takes an array of NSString*.
78- (void)openFiles:(NSArray*)filename;
79
80// Terminate immediately. This is necessary as we override terminate: to send
81// a QuitApp message.
82- (void)terminateNow;
83
84@end
85
86// The AppShimController is responsible for communication with the main Chrome
87// process, and generally controls the lifetime of the app shim process.
88class AppShimController : public IPC::Listener {
89 public:
90  AppShimController();
91  virtual ~AppShimController();
92
93  // Called when the main Chrome process responds to the Apple Event ping that
94  // was sent, or when the ping fails (if |success| is false).
95  void OnPingChromeReply(bool success);
96
97  // Called |kPingChromeTimeoutSeconds| after startup, to allow a timeout on the
98  // ping event to be detected.
99  void OnPingChromeTimeout();
100
101  // Connects to Chrome and sends a LaunchApp message.
102  void Init();
103
104  // Create a channel from |socket_path| and send a LaunchApp message.
105  void CreateChannelAndSendLaunchApp(const base::FilePath& socket_path);
106
107  // Builds main menu bar items.
108  void SetUpMenu();
109
110  void SendSetAppHidden(bool hidden);
111
112  void SendQuitApp();
113
114  // Called when the app is activated, e.g. by clicking on it in the dock, by
115  // dropping a file on the dock icon, or by Cmd+Tabbing to it.
116  // Returns whether the message was sent.
117  bool SendFocusApp(apps::AppShimFocusType focus_type,
118                    const std::vector<base::FilePath>& files);
119
120 private:
121  // IPC::Listener implemetation.
122  virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
123  virtual void OnChannelError() OVERRIDE;
124
125  // If Chrome failed to launch the app, |success| will be false and the app
126  // shim process should die.
127  void OnLaunchAppDone(apps::AppShimLaunchResult result);
128
129  // Hide this app.
130  void OnHide();
131
132  // Requests user attention.
133  void OnRequestUserAttention();
134
135  // Terminates the app shim process.
136  void Close();
137
138  base::FilePath user_data_dir_;
139  scoped_ptr<IPC::ChannelProxy> channel_;
140  base::scoped_nsobject<AppShimDelegate> delegate_;
141  bool launch_app_done_;
142  bool ping_chrome_reply_received_;
143
144  DISALLOW_COPY_AND_ASSIGN(AppShimController);
145};
146
147AppShimController::AppShimController()
148    : delegate_([[AppShimDelegate alloc] init]),
149      launch_app_done_(false),
150      ping_chrome_reply_received_(false) {
151  // Since AppShimController is created before the main message loop starts,
152  // NSApp will not be set, so use sharedApplication.
153  [[NSApplication sharedApplication] setDelegate:delegate_];
154}
155
156AppShimController::~AppShimController() {
157  // Un-set the delegate since NSApplication does not retain it.
158  [[NSApplication sharedApplication] setDelegate:nil];
159}
160
161void AppShimController::OnPingChromeReply(bool success) {
162  ping_chrome_reply_received_ = true;
163  if (!success) {
164    [NSApp terminate:nil];
165    return;
166  }
167
168  Init();
169}
170
171void AppShimController::OnPingChromeTimeout() {
172  if (!ping_chrome_reply_received_)
173    [NSApp terminate:nil];
174}
175
176void AppShimController::Init() {
177  DCHECK(g_io_thread);
178
179  SetUpMenu();
180
181  // Chrome will relaunch shims when relaunching apps.
182  if (base::mac::IsOSLionOrLater())
183    [NSApp disableRelaunchOnLogin];
184
185  // The user_data_dir for shims actually contains the app_data_path.
186  // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
187  user_data_dir_ = g_info->user_data_dir.DirName().DirName().DirName();
188  CHECK(!user_data_dir_.empty());
189
190  base::FilePath symlink_path =
191      user_data_dir_.Append(app_mode::kAppShimSocketSymlinkName);
192
193  base::FilePath socket_path;
194  if (!base::ReadSymbolicLink(symlink_path, &socket_path)) {
195    // The path in the user data dir is not a symlink, try connecting directly.
196    CreateChannelAndSendLaunchApp(symlink_path);
197    return;
198  }
199
200  app_mode::VerifySocketPermissions(socket_path);
201
202  CreateChannelAndSendLaunchApp(socket_path);
203}
204
205void AppShimController::CreateChannelAndSendLaunchApp(
206    const base::FilePath& socket_path) {
207  IPC::ChannelHandle handle(socket_path.value());
208  channel_ = IPC::ChannelProxy::Create(handle,
209                                       IPC::Channel::MODE_NAMED_CLIENT,
210                                       this,
211                                       g_io_thread->message_loop_proxy().get());
212
213  bool launched_by_chrome =
214      CommandLine::ForCurrentProcess()->HasSwitch(
215          app_mode::kLaunchedByChromeProcessId);
216  apps::AppShimLaunchType launch_type = launched_by_chrome ?
217          apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL;
218
219  [delegate_ setController:this];
220
221  std::vector<base::FilePath> files;
222  [delegate_ getFilesToOpenAtStartup:&files];
223
224  channel_->Send(new AppShimHostMsg_LaunchApp(
225      g_info->profile_dir, g_info->app_mode_id, launch_type, files));
226}
227
228void AppShimController::SetUpMenu() {
229  NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name);
230
231  // Create a main menu since [NSApp mainMenu] is nil.
232  base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]);
233
234  // The title of the first item is replaced by OSX with the name of the app and
235  // bold styling. Create a dummy item for this and make it hidden.
236  NSMenuItem* dummy_item = [main_menu addItemWithTitle:title
237                                                action:nil
238                                         keyEquivalent:@""];
239  base::scoped_nsobject<NSMenu> dummy_submenu(
240      [[NSMenu alloc] initWithTitle:title]);
241  [dummy_item setSubmenu:dummy_submenu];
242  [dummy_item setHidden:YES];
243
244  // Construct an unbolded app menu, to match how it appears in the Chrome menu
245  // bar when the app is focused.
246  NSMenuItem* item = [main_menu addItemWithTitle:title
247                                          action:nil
248                                   keyEquivalent:@""];
249  base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]);
250  [item setSubmenu:submenu];
251
252  // Add a quit entry.
253  NSString* quit_localized_string =
254      l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name);
255  [submenu addItemWithTitle:quit_localized_string
256                     action:@selector(terminate:)
257              keyEquivalent:@"q"];
258
259  // Add File, Edit, and Window menus. These are just here to make the
260  // transition smoother, i.e. from another application to the shim then to
261  // Chrome.
262  [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC)
263                       action:nil
264                keyEquivalent:@""];
265  [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC)
266                       action:nil
267                keyEquivalent:@""];
268  [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC)
269                       action:nil
270                keyEquivalent:@""];
271
272  [NSApp setMainMenu:main_menu];
273}
274
275void AppShimController::SendQuitApp() {
276  channel_->Send(new AppShimHostMsg_QuitApp);
277}
278
279bool AppShimController::OnMessageReceived(const IPC::Message& message) {
280  bool handled = true;
281  IPC_BEGIN_MESSAGE_MAP(AppShimController, message)
282    IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone)
283    IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide)
284    IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention)
285    IPC_MESSAGE_UNHANDLED(handled = false)
286  IPC_END_MESSAGE_MAP()
287
288  return handled;
289}
290
291void AppShimController::OnChannelError() {
292  Close();
293}
294
295void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) {
296  if (result != apps::APP_SHIM_LAUNCH_SUCCESS) {
297    Close();
298    return;
299  }
300
301  std::vector<base::FilePath> files;
302  if ([delegate_ getFilesToOpenAtStartup:&files])
303    SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files);
304
305  launch_app_done_ = true;
306}
307
308void AppShimController::OnHide() {
309  [NSApp hide:nil];
310}
311
312void AppShimController::OnRequestUserAttention() {
313  [NSApp requestUserAttention:NSInformationalRequest];
314}
315
316void AppShimController::Close() {
317  [delegate_ terminateNow];
318}
319
320bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type,
321                                     const std::vector<base::FilePath>& files) {
322  if (launch_app_done_) {
323    channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files));
324    return true;
325  }
326
327  return false;
328}
329
330void AppShimController::SendSetAppHidden(bool hidden) {
331  channel_->Send(new AppShimHostMsg_SetAppHidden(hidden));
332}
333
334@implementation AppShimDelegate
335
336- (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out {
337  if (filesToOpenAtStartup_.empty())
338    return NO;
339
340  out->insert(out->end(),
341              filesToOpenAtStartup_.begin(),
342              filesToOpenAtStartup_.end());
343  filesToOpenAtStartup_.clear();
344  return YES;
345}
346
347- (void)setController:(AppShimController*)controller {
348  appShimController_ = controller;
349}
350
351- (void)openFiles:(NSArray*)filenames {
352  std::vector<base::FilePath> filePaths;
353  for (NSString* filename in filenames)
354    filePaths.push_back(base::mac::NSStringToFilePath(filename));
355
356  // If the AppShimController is ready, try to send a FocusApp. If that fails,
357  // (e.g. if launching has not finished), enqueue the files.
358  if (appShimController_ &&
359      appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES,
360                                       filePaths)) {
361    return;
362  }
363
364  filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(),
365                               filePaths.begin(),
366                               filePaths.end());
367}
368
369- (BOOL)application:(NSApplication*)app
370           openFile:(NSString*)filename {
371  [self openFiles:@[filename]];
372  return YES;
373}
374
375- (void)application:(NSApplication*)app
376          openFiles:(NSArray*)filenames {
377  [self openFiles:filenames];
378  [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
379}
380
381- (BOOL)applicationOpenUntitledFile:(NSApplication*)app {
382  if (appShimController_) {
383    return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN,
384                                            std::vector<base::FilePath>());
385  }
386
387  return NO;
388}
389
390- (void)applicationWillBecomeActive:(NSNotification*)notification {
391  if (appShimController_) {
392    appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL,
393                                     std::vector<base::FilePath>());
394  }
395}
396
397- (NSApplicationTerminateReply)
398    applicationShouldTerminate:(NSApplication*)sender {
399  if (terminateNow_ || !appShimController_)
400    return NSTerminateNow;
401
402  appShimController_->SendQuitApp();
403  // Wait for the channel to close before terminating.
404  terminateRequested_ = YES;
405  return NSTerminateLater;
406}
407
408- (void)applicationWillHide:(NSNotification*)notification {
409  if (appShimController_)
410    appShimController_->SendSetAppHidden(true);
411}
412
413- (void)applicationWillUnhide:(NSNotification*)notification {
414  if (appShimController_)
415    appShimController_->SendSetAppHidden(false);
416}
417
418- (void)terminateNow {
419  if (terminateRequested_) {
420    [NSApp replyToApplicationShouldTerminate:NSTerminateNow];
421    return;
422  }
423
424  terminateNow_ = YES;
425  [NSApp terminate:nil];
426}
427
428@end
429
430//-----------------------------------------------------------------------------
431
432// A ReplyEventHandler is a helper class to send an Apple Event to a process
433// and call a callback when the reply returns.
434//
435// This is used to 'ping' the main Chrome process -- once Chrome has sent back
436// an Apple Event reply, it's guaranteed that it has opened the IPC channel
437// that the app shim will connect to.
438@interface ReplyEventHandler : NSObject {
439  base::Callback<void(bool)> onReply_;
440  AEDesc replyEvent_;
441}
442// Sends an Apple Event to the process identified by |psn|, and calls |replyFn|
443// when the reply is received. Internally this creates a ReplyEventHandler,
444// which will delete itself once the reply event has been received.
445+ (void)pingProcess:(const ProcessSerialNumber&)psn
446            andCall:(base::Callback<void(bool)>)replyFn;
447@end
448
449@interface ReplyEventHandler (PrivateMethods)
450// Initialise the reply event handler. Doesn't register any handlers until
451// |-pingProcess:| is called. |replyFn| is the function to be called when the
452// Apple Event reply arrives.
453- (id)initWithCallback:(base::Callback<void(bool)>)replyFn;
454
455// Sends an Apple Event ping to the process identified by |psn| and registers
456// to listen for a reply.
457- (void)pingProcess:(const ProcessSerialNumber&)psn;
458
459// Called when a response is received from the target process for the ping sent
460// by |-pingProcess:|.
461- (void)message:(NSAppleEventDescriptor*)event
462      withReply:(NSAppleEventDescriptor*)reply;
463
464// Calls |onReply_|, passing it |success| to specify whether the ping was
465// successful.
466- (void)closeWithSuccess:(bool)success;
467@end
468
469@implementation ReplyEventHandler
470+ (void)pingProcess:(const ProcessSerialNumber&)psn
471            andCall:(base::Callback<void(bool)>)replyFn {
472  // The object will release itself when the reply arrives, or possibly earlier
473  // if an unrecoverable error occurs.
474  ReplyEventHandler* handler =
475      [[ReplyEventHandler alloc] initWithCallback:replyFn];
476  [handler pingProcess:psn];
477}
478@end
479
480@implementation ReplyEventHandler (PrivateMethods)
481- (id)initWithCallback:(base::Callback<void(bool)>)replyFn {
482  if ((self = [super init])) {
483    onReply_ = replyFn;
484  }
485  return self;
486}
487
488- (void)pingProcess:(const ProcessSerialNumber&)psn {
489  // Register the reply listener.
490  NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
491  [em setEventHandler:self
492          andSelector:@selector(message:withReply:)
493        forEventClass:'aevt'
494           andEventID:'ansr'];
495  // Craft the Apple Event to send.
496  NSAppleEventDescriptor* target = [NSAppleEventDescriptor
497      descriptorWithDescriptorType:typeProcessSerialNumber
498                             bytes:&psn
499                            length:sizeof(psn)];
500  NSAppleEventDescriptor* initial_event =
501      [NSAppleEventDescriptor
502          appleEventWithEventClass:app_mode::kAEChromeAppClass
503                           eventID:app_mode::kAEChromeAppPing
504                  targetDescriptor:target
505                          returnID:kAutoGenerateReturnID
506                     transactionID:kAnyTransactionID];
507
508  // Note that AESendMessage effectively ignores kAEDefaultTimeout, because this
509  // call does not pass kAEWantReceipt (which is deprecated and unsupported on
510  // Mac). Instead, rely on OnPingChromeTimeout().
511  OSStatus status = AESendMessage(
512      [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout);
513  if (status != noErr) {
514    OSSTATUS_LOG(ERROR, status) << "AESendMessage";
515    [self closeWithSuccess:false];
516  }
517}
518
519- (void)message:(NSAppleEventDescriptor*)event
520      withReply:(NSAppleEventDescriptor*)reply {
521  [self closeWithSuccess:true];
522}
523
524- (void)closeWithSuccess:(bool)success {
525  onReply_.Run(success);
526  NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
527  [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr'];
528  [self release];
529}
530@end
531
532//-----------------------------------------------------------------------------
533
534extern "C" {
535
536// |ChromeAppModeStart()| is the point of entry into the framework from the app
537// mode loader.
538__attribute__((visibility("default")))
539int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info);
540
541}  // extern "C"
542
543int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
544  CommandLine::Init(info->argc, info->argv);
545
546  base::mac::ScopedNSAutoreleasePool scoped_pool;
547  base::AtExitManager exit_manager;
548  chrome::RegisterPathProvider();
549
550  if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
551    RAW_LOG(ERROR, "App Mode Loader too old.");
552    return 1;
553  }
554  if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
555    RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
556    return 1;
557  }
558
559  g_info = info;
560
561  // Set bundle paths. This loads the bundles.
562  base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path);
563  base::mac::SetOverrideFrameworkBundlePath(
564      g_info->chrome_versioned_path.Append(chrome::kFrameworkName));
565
566  // Calculate the preferred locale used by Chrome.
567  // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls
568  // [base::mac::OuterBundle() preferredLocalizations] which gets localizations
569  // from the bundle of the running app (i.e. it is equivalent to
570  // [[NSBundle mainBundle] preferredLocalizations]) instead of the target
571  // bundle.
572  NSArray* preferred_languages = [NSLocale preferredLanguages];
573  NSArray* supported_languages = [base::mac::OuterBundle() localizations];
574  std::string preferred_localization;
575  for (NSString* language in preferred_languages) {
576    if ([supported_languages containsObject:language]) {
577      preferred_localization = base::SysNSStringToUTF8(language);
578      break;
579    }
580  }
581  std::string locale = l10n_util::NormalizeLocale(
582      l10n_util::GetApplicationLocale(preferred_localization));
583
584  // Load localized strings.
585  ResourceBundle::InitSharedInstanceLocaleOnly(locale, NULL);
586
587  // Launch the IO thread.
588  base::Thread::Options io_thread_options;
589  io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO;
590  base::Thread *io_thread = new base::Thread("CrAppShimIO");
591  io_thread->StartWithOptions(io_thread_options);
592  g_io_thread = io_thread;
593
594  // Find already running instances of Chrome.
595  pid_t pid = -1;
596  std::string chrome_process_id = CommandLine::ForCurrentProcess()->
597      GetSwitchValueASCII(app_mode::kLaunchedByChromeProcessId);
598  if (!chrome_process_id.empty()) {
599    if (!base::StringToInt(chrome_process_id, &pid))
600      LOG(FATAL) << "Invalid PID: " << chrome_process_id;
601  } else {
602    NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier];
603    NSArray* existing_chrome = [NSRunningApplication
604        runningApplicationsWithBundleIdentifier:chrome_bundle_id];
605    if ([existing_chrome count] > 0)
606      pid = [[existing_chrome objectAtIndex:0] processIdentifier];
607  }
608
609  AppShimController controller;
610  base::MessageLoopForUI main_message_loop;
611  main_message_loop.set_thread_name("MainThread");
612  base::PlatformThread::SetName("CrAppShimMain");
613
614  // In tests, launching Chrome does nothing, and we won't get a ping response,
615  // so just assume the socket exists.
616  if (pid == -1 &&
617      !CommandLine::ForCurrentProcess()->HasSwitch(
618          app_mode::kLaunchedForTest)) {
619    // Launch Chrome if it isn't already running.
620    ProcessSerialNumber psn;
621    CommandLine command_line(CommandLine::NO_PROGRAM);
622    command_line.AppendSwitch(switches::kSilentLaunch);
623
624    // If the shim is the app launcher, pass --show-app-list when starting a new
625    // Chrome process to inform startup codepaths and load the correct profile.
626    if (info->app_mode_id == app_mode::kAppListModeId) {
627      command_line.AppendSwitch(switches::kShowAppList);
628    } else {
629      command_line.AppendSwitchPath(switches::kProfileDirectory,
630                                    info->profile_dir);
631    }
632
633    bool success =
634        base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(),
635                                           command_line,
636                                           kLSLaunchDefaults,
637                                           &psn);
638    if (!success)
639      return 1;
640
641    base::Callback<void(bool)> on_ping_chrome_reply =
642        base::Bind(&AppShimController::OnPingChromeReply,
643                   base::Unretained(&controller));
644
645    // This code abuses the fact that Apple Events sent before the process is
646    // fully initialized don't receive a reply until its run loop starts. Once
647    // the reply is received, Chrome will have opened its IPC port, guaranteed.
648    [ReplyEventHandler pingProcess:psn
649                           andCall:on_ping_chrome_reply];
650
651    main_message_loop.PostDelayedTask(
652        FROM_HERE,
653        base::Bind(&AppShimController::OnPingChromeTimeout,
654                   base::Unretained(&controller)),
655        base::TimeDelta::FromSeconds(kPingChromeTimeoutSeconds));
656  } else {
657    // Chrome already running. Proceed to init. This could still fail if Chrome
658    // is still starting up or shutting down, but the process will exit quickly,
659    // which is preferable to waiting for the Apple Event to timeout after one
660    // minute.
661    main_message_loop.PostTask(
662        FROM_HERE,
663        base::Bind(&AppShimController::Init,
664                   base::Unretained(&controller)));
665  }
666
667  main_message_loop.Run();
668  return 0;
669}
670