summaryrefslogtreecommitdiffstats
path: root/chrome/app_shim/chrome_main_app_mode_mac.mm
blob: f2d24c7df59ee909526d171341abe810c2965d6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
// 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/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();
  ~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<base::FilePath>& 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();

  // Set this app to the unhidden state. Happens when an app window shows
  // itself.
  void OnUnhideWithoutActivation();

  // 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 = [NSApplication sharedApplication];
  [sharedApplication setDelegate:delegate_];
}

AppShimController::~AppShimController() {
  // Un-set the delegate since NSApplication does not retain it.
  NSApplication* sharedApplication = [NSApplication sharedApplication];
  [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->task_runner().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<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_UnhideWithoutActivation,
                        OnUnhideWithoutActivation)
    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::OnUnhideWithoutActivation() {
  [NSApp unhideWithoutActivation];
}

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. There are cases where the Chromium framework may have changed in
// a way that is incompatible with an older shim (e.g. change to libc++ library
// linking). The function name is versioned to provide a way to force shim
// upgrades if they are launched before an updated version of Chromium can
// upgrade them; the old shim will not be able to dyload the new
// ChromeAppModeStart, so it will fall back to the upgrade path. See
// https://crbug.com/561205.
__attribute__((visibility("default")))
int ChromeAppModeStart_v4(const app_mode::ChromeAppModeInfo* info);

}  // extern "C"

int ChromeAppModeStart_v4(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<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;
}