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.
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
10 #import <Cocoa/Cocoa.h>
13 #include "base/at_exit.h"
14 #include "base/command_line.h"
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/logging.h"
18 #include "base/mac/bundle_locations.h"
19 #include "base/mac/foundation_util.h"
20 #include "base/mac/launch_services_util.h"
21 #include "base/mac/mac_logging.h"
22 #include "base/mac/mac_util.h"
23 #include "base/mac/scoped_nsautorelease_pool.h"
24 #include "base/mac/scoped_nsobject.h"
25 #include "base/mac/sdk_forward_declarations.h"
26 #include "base/message_loop/message_loop.h"
27 #include "base/strings/string_number_conversions.h"
28 #include "base/strings/sys_string_conversions.h"
29 #include "base/threading/thread.h"
30 #include "chrome/common/chrome_constants.h"
31 #include "chrome/common/chrome_paths.h"
32 #include "chrome/common/chrome_switches.h"
33 #include "chrome/common/mac/app_mode_common.h"
34 #include "chrome/common/mac/app_shim_messages.h"
35 #include "chrome/grit/generated_resources.h"
36 #include "ipc/ipc_channel_proxy.h"
37 #include "ipc/ipc_listener.h"
38 #include "ipc/ipc_message.h"
39 #include "ui/base/l10n/l10n_util.h"
40 #include "ui/base/resource/resource_bundle.h"
44 // Timeout in seconds to wait for a reply for the initial Apple Event. Note that
45 // kAEDefaultTimeout on Mac is "about one minute" according to Apple's
46 // documentation, but is no longer supported for asynchronous Apple Events.
47 const int kPingChromeTimeoutSeconds = 60;
49 const app_mode::ChromeAppModeInfo* g_info;
50 base::Thread* g_io_thread = NULL;
54 class AppShimController;
56 // An application delegate to catch user interactions and send the appropriate
57 // IPC messages to Chrome.
58 @interface AppShimDelegate : NSObject<NSApplicationDelegate> {
60 AppShimController* appShimController_; // Weak, initially NULL.
62 BOOL terminateRequested_;
63 std::vector<base::FilePath> filesToOpenAtStartup_;
66 // The controller is initially NULL. Setting it indicates to the delegate that
67 // the controller has finished initialization.
68 - (void)setController:(AppShimController*)controller;
70 // Gets files that were queued because the controller was not ready.
71 // Returns whether any FilePaths were added to |out|.
72 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out;
74 // If the controller is ready, this sends a FocusApp with the files to open.
75 // Otherwise, this adds the files to |filesToOpenAtStartup_|.
76 // Takes an array of NSString*.
77 - (void)openFiles:(NSArray*)filename;
79 // Terminate immediately. This is necessary as we override terminate: to send
85 // The AppShimController is responsible for communication with the main Chrome
86 // process, and generally controls the lifetime of the app shim process.
87 class AppShimController : public IPC::Listener {
90 ~AppShimController() override;
92 // Called when the main Chrome process responds to the Apple Event ping that
93 // was sent, or when the ping fails (if |success| is false).
94 void OnPingChromeReply(bool success);
96 // Called |kPingChromeTimeoutSeconds| after startup, to allow a timeout on the
97 // ping event to be detected.
98 void OnPingChromeTimeout();
100 // Connects to Chrome and sends a LaunchApp message.
103 // Create a channel from |socket_path| and send a LaunchApp message.
104 void CreateChannelAndSendLaunchApp(const base::FilePath& socket_path);
106 // Builds main menu bar items.
109 void SendSetAppHidden(bool hidden);
113 // Called when the app is activated, e.g. by clicking on it in the dock, by
114 // dropping a file on the dock icon, or by Cmd+Tabbing to it.
115 // Returns whether the message was sent.
116 bool SendFocusApp(apps::AppShimFocusType focus_type,
117 const std::vector<base::FilePath>& files);
120 // IPC::Listener implemetation.
121 bool OnMessageReceived(const IPC::Message& message) override;
122 void OnChannelError() override;
124 // If Chrome failed to launch the app, |success| will be false and the app
125 // shim process should die.
126 void OnLaunchAppDone(apps::AppShimLaunchResult result);
131 // Set this app to the unhidden state. Happens when an app window shows
133 void OnUnhideWithoutActivation();
135 // Requests user attention.
136 void OnRequestUserAttention();
137 void OnSetUserAttention(apps::AppShimAttentionType attention_type);
139 // Terminates the app shim process.
142 base::FilePath user_data_dir_;
143 scoped_ptr<IPC::ChannelProxy> channel_;
144 base::scoped_nsobject<AppShimDelegate> delegate_;
145 bool launch_app_done_;
146 bool ping_chrome_reply_received_;
147 NSInteger attention_request_id_;
149 DISALLOW_COPY_AND_ASSIGN(AppShimController);
152 AppShimController::AppShimController()
153 : delegate_([[AppShimDelegate alloc] init]),
154 launch_app_done_(false),
155 ping_chrome_reply_received_(false),
156 attention_request_id_(0) {
157 // Since AppShimController is created before the main message loop starts,
158 // NSApp will not be set, so use sharedApplication.
159 [[NSApplication sharedApplication] setDelegate:delegate_];
162 AppShimController::~AppShimController() {
163 // Un-set the delegate since NSApplication does not retain it.
164 [[NSApplication sharedApplication] setDelegate:nil];
167 void AppShimController::OnPingChromeReply(bool success) {
168 ping_chrome_reply_received_ = true;
170 [NSApp terminate:nil];
177 void AppShimController::OnPingChromeTimeout() {
178 if (!ping_chrome_reply_received_)
179 [NSApp terminate:nil];
182 void AppShimController::Init() {
187 // Chrome will relaunch shims when relaunching apps.
188 if (base::mac::IsOSLionOrLater())
189 [NSApp disableRelaunchOnLogin];
191 // The user_data_dir for shims actually contains the app_data_path.
192 // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
193 user_data_dir_ = g_info->user_data_dir.DirName().DirName().DirName();
194 CHECK(!user_data_dir_.empty());
196 base::FilePath symlink_path =
197 user_data_dir_.Append(app_mode::kAppShimSocketSymlinkName);
199 base::FilePath socket_path;
200 if (!base::ReadSymbolicLink(symlink_path, &socket_path)) {
201 // The path in the user data dir is not a symlink, try connecting directly.
202 CreateChannelAndSendLaunchApp(symlink_path);
206 app_mode::VerifySocketPermissions(socket_path);
208 CreateChannelAndSendLaunchApp(socket_path);
211 void AppShimController::CreateChannelAndSendLaunchApp(
212 const base::FilePath& socket_path) {
213 IPC::ChannelHandle handle(socket_path.value());
214 channel_ = IPC::ChannelProxy::Create(handle,
215 IPC::Channel::MODE_NAMED_CLIENT,
217 g_io_thread->message_loop_proxy().get());
219 bool launched_by_chrome = base::CommandLine::ForCurrentProcess()->HasSwitch(
220 app_mode::kLaunchedByChromeProcessId);
221 apps::AppShimLaunchType launch_type = launched_by_chrome ?
222 apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL;
224 [delegate_ setController:this];
226 std::vector<base::FilePath> files;
227 [delegate_ getFilesToOpenAtStartup:&files];
229 channel_->Send(new AppShimHostMsg_LaunchApp(
230 g_info->profile_dir, g_info->app_mode_id, launch_type, files));
233 void AppShimController::SetUpMenu() {
234 NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name);
236 // Create a main menu since [NSApp mainMenu] is nil.
237 base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]);
239 // The title of the first item is replaced by OSX with the name of the app and
240 // bold styling. Create a dummy item for this and make it hidden.
241 NSMenuItem* dummy_item = [main_menu addItemWithTitle:title
244 base::scoped_nsobject<NSMenu> dummy_submenu(
245 [[NSMenu alloc] initWithTitle:title]);
246 [dummy_item setSubmenu:dummy_submenu];
247 [dummy_item setHidden:YES];
249 // Construct an unbolded app menu, to match how it appears in the Chrome menu
250 // bar when the app is focused.
251 NSMenuItem* item = [main_menu addItemWithTitle:title
254 base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]);
255 [item setSubmenu:submenu];
258 NSString* quit_localized_string =
259 l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name);
260 [submenu addItemWithTitle:quit_localized_string
261 action:@selector(terminate:)
264 // Add File, Edit, and Window menus. These are just here to make the
265 // transition smoother, i.e. from another application to the shim then to
267 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC)
270 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC)
273 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC)
277 [NSApp setMainMenu:main_menu];
280 void AppShimController::SendQuitApp() {
281 channel_->Send(new AppShimHostMsg_QuitApp);
284 bool AppShimController::OnMessageReceived(const IPC::Message& message) {
286 IPC_BEGIN_MESSAGE_MAP(AppShimController, message)
287 IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone)
288 IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide)
289 IPC_MESSAGE_HANDLER(AppShimMsg_UnhideWithoutActivation,
290 OnUnhideWithoutActivation)
291 IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention)
292 IPC_MESSAGE_HANDLER(AppShimMsg_SetUserAttention, OnSetUserAttention)
293 IPC_MESSAGE_UNHANDLED(handled = false)
294 IPC_END_MESSAGE_MAP()
299 void AppShimController::OnChannelError() {
303 void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) {
304 if (result != apps::APP_SHIM_LAUNCH_SUCCESS) {
309 std::vector<base::FilePath> files;
310 if ([delegate_ getFilesToOpenAtStartup:&files])
311 SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files);
313 launch_app_done_ = true;
316 void AppShimController::OnHide() {
320 void AppShimController::OnUnhideWithoutActivation() {
321 [NSApp unhideWithoutActivation];
324 void AppShimController::OnRequestUserAttention() {
325 OnSetUserAttention(apps::APP_SHIM_ATTENTION_INFORMATIONAL);
328 void AppShimController::OnSetUserAttention(
329 apps::AppShimAttentionType attention_type) {
330 switch (attention_type) {
331 case apps::APP_SHIM_ATTENTION_CANCEL:
332 [NSApp cancelUserAttentionRequest:attention_request_id_];
333 attention_request_id_ = 0;
335 case apps::APP_SHIM_ATTENTION_CRITICAL:
336 attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest];
338 case apps::APP_SHIM_ATTENTION_INFORMATIONAL:
339 attention_request_id_ =
340 [NSApp requestUserAttention:NSInformationalRequest];
342 case apps::APP_SHIM_ATTENTION_NUM_TYPES:
347 void AppShimController::Close() {
348 [delegate_ terminateNow];
351 bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type,
352 const std::vector<base::FilePath>& files) {
353 if (launch_app_done_) {
354 channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files));
361 void AppShimController::SendSetAppHidden(bool hidden) {
362 channel_->Send(new AppShimHostMsg_SetAppHidden(hidden));
365 @implementation AppShimDelegate
367 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out {
368 if (filesToOpenAtStartup_.empty())
371 out->insert(out->end(),
372 filesToOpenAtStartup_.begin(),
373 filesToOpenAtStartup_.end());
374 filesToOpenAtStartup_.clear();
378 - (void)setController:(AppShimController*)controller {
379 appShimController_ = controller;
382 - (void)openFiles:(NSArray*)filenames {
383 std::vector<base::FilePath> filePaths;
384 for (NSString* filename in filenames)
385 filePaths.push_back(base::mac::NSStringToFilePath(filename));
387 // If the AppShimController is ready, try to send a FocusApp. If that fails,
388 // (e.g. if launching has not finished), enqueue the files.
389 if (appShimController_ &&
390 appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES,
395 filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(),
400 - (BOOL)application:(NSApplication*)app
401 openFile:(NSString*)filename {
402 [self openFiles:@[filename]];
406 - (void)application:(NSApplication*)app
407 openFiles:(NSArray*)filenames {
408 [self openFiles:filenames];
409 [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
412 - (BOOL)applicationOpenUntitledFile:(NSApplication*)app {
413 if (appShimController_) {
414 return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN,
415 std::vector<base::FilePath>());
421 - (void)applicationWillBecomeActive:(NSNotification*)notification {
422 if (appShimController_) {
423 appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL,
424 std::vector<base::FilePath>());
428 - (NSApplicationTerminateReply)
429 applicationShouldTerminate:(NSApplication*)sender {
430 if (terminateNow_ || !appShimController_)
431 return NSTerminateNow;
433 appShimController_->SendQuitApp();
434 // Wait for the channel to close before terminating.
435 terminateRequested_ = YES;
436 return NSTerminateLater;
439 - (void)applicationWillHide:(NSNotification*)notification {
440 if (appShimController_)
441 appShimController_->SendSetAppHidden(true);
444 - (void)applicationWillUnhide:(NSNotification*)notification {
445 if (appShimController_)
446 appShimController_->SendSetAppHidden(false);
449 - (void)terminateNow {
450 if (terminateRequested_) {
451 [NSApp replyToApplicationShouldTerminate:NSTerminateNow];
456 [NSApp terminate:nil];
461 //-----------------------------------------------------------------------------
463 // A ReplyEventHandler is a helper class to send an Apple Event to a process
464 // and call a callback when the reply returns.
466 // This is used to 'ping' the main Chrome process -- once Chrome has sent back
467 // an Apple Event reply, it's guaranteed that it has opened the IPC channel
468 // that the app shim will connect to.
469 @interface ReplyEventHandler : NSObject {
470 base::Callback<void(bool)> onReply_;
473 // Sends an Apple Event to the process identified by |psn|, and calls |replyFn|
474 // when the reply is received. Internally this creates a ReplyEventHandler,
475 // which will delete itself once the reply event has been received.
476 + (void)pingProcess:(const ProcessSerialNumber&)psn
477 andCall:(base::Callback<void(bool)>)replyFn;
480 @interface ReplyEventHandler (PrivateMethods)
481 // Initialise the reply event handler. Doesn't register any handlers until
482 // |-pingProcess:| is called. |replyFn| is the function to be called when the
483 // Apple Event reply arrives.
484 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn;
486 // Sends an Apple Event ping to the process identified by |psn| and registers
487 // to listen for a reply.
488 - (void)pingProcess:(const ProcessSerialNumber&)psn;
490 // Called when a response is received from the target process for the ping sent
491 // by |-pingProcess:|.
492 - (void)message:(NSAppleEventDescriptor*)event
493 withReply:(NSAppleEventDescriptor*)reply;
495 // Calls |onReply_|, passing it |success| to specify whether the ping was
497 - (void)closeWithSuccess:(bool)success;
500 @implementation ReplyEventHandler
501 + (void)pingProcess:(const ProcessSerialNumber&)psn
502 andCall:(base::Callback<void(bool)>)replyFn {
503 // The object will release itself when the reply arrives, or possibly earlier
504 // if an unrecoverable error occurs.
505 ReplyEventHandler* handler =
506 [[ReplyEventHandler alloc] initWithCallback:replyFn];
507 [handler pingProcess:psn];
511 @implementation ReplyEventHandler (PrivateMethods)
512 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn {
513 if ((self = [super init])) {
519 - (void)pingProcess:(const ProcessSerialNumber&)psn {
520 // Register the reply listener.
521 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
522 [em setEventHandler:self
523 andSelector:@selector(message:withReply:)
526 // Craft the Apple Event to send.
527 NSAppleEventDescriptor* target = [NSAppleEventDescriptor
528 descriptorWithDescriptorType:typeProcessSerialNumber
531 NSAppleEventDescriptor* initial_event =
532 [NSAppleEventDescriptor
533 appleEventWithEventClass:app_mode::kAEChromeAppClass
534 eventID:app_mode::kAEChromeAppPing
535 targetDescriptor:target
536 returnID:kAutoGenerateReturnID
537 transactionID:kAnyTransactionID];
539 // Note that AESendMessage effectively ignores kAEDefaultTimeout, because this
540 // call does not pass kAEWantReceipt (which is deprecated and unsupported on
541 // Mac). Instead, rely on OnPingChromeTimeout().
542 OSStatus status = AESendMessage(
543 [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout);
544 if (status != noErr) {
545 OSSTATUS_LOG(ERROR, status) << "AESendMessage";
546 [self closeWithSuccess:false];
550 - (void)message:(NSAppleEventDescriptor*)event
551 withReply:(NSAppleEventDescriptor*)reply {
552 [self closeWithSuccess:true];
555 - (void)closeWithSuccess:(bool)success {
556 onReply_.Run(success);
557 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
558 [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr'];
563 //-----------------------------------------------------------------------------
567 // |ChromeAppModeStart()| is the point of entry into the framework from the app
569 __attribute__((visibility("default")))
570 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info);
574 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
575 base::CommandLine::Init(info->argc, info->argv);
577 base::mac::ScopedNSAutoreleasePool scoped_pool;
578 base::AtExitManager exit_manager;
579 chrome::RegisterPathProvider();
581 if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
582 RAW_LOG(ERROR, "App Mode Loader too old.");
585 if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
586 RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
592 // Set bundle paths. This loads the bundles.
593 base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path);
594 base::mac::SetOverrideFrameworkBundlePath(
595 g_info->chrome_versioned_path.Append(chrome::kFrameworkName));
597 // Calculate the preferred locale used by Chrome.
598 // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls
599 // [base::mac::OuterBundle() preferredLocalizations] which gets localizations
600 // from the bundle of the running app (i.e. it is equivalent to
601 // [[NSBundle mainBundle] preferredLocalizations]) instead of the target
603 NSArray* preferred_languages = [NSLocale preferredLanguages];
604 NSArray* supported_languages = [base::mac::OuterBundle() localizations];
605 std::string preferred_localization;
606 for (NSString* language in preferred_languages) {
607 if ([supported_languages containsObject:language]) {
608 preferred_localization = base::SysNSStringToUTF8(language);
612 std::string locale = l10n_util::NormalizeLocale(
613 l10n_util::GetApplicationLocale(preferred_localization));
615 // Load localized strings.
616 ui::ResourceBundle::InitSharedInstanceWithLocale(
617 locale, NULL, ui::ResourceBundle::DO_NOT_LOAD_COMMON_RESOURCES);
619 // Launch the IO thread.
620 base::Thread::Options io_thread_options;
621 io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO;
622 base::Thread *io_thread = new base::Thread("CrAppShimIO");
623 io_thread->StartWithOptions(io_thread_options);
624 g_io_thread = io_thread;
626 // Find already running instances of Chrome.
628 std::string chrome_process_id =
629 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
630 app_mode::kLaunchedByChromeProcessId);
631 if (!chrome_process_id.empty()) {
632 if (!base::StringToInt(chrome_process_id, &pid))
633 LOG(FATAL) << "Invalid PID: " << chrome_process_id;
635 NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier];
636 NSArray* existing_chrome = [NSRunningApplication
637 runningApplicationsWithBundleIdentifier:chrome_bundle_id];
638 if ([existing_chrome count] > 0)
639 pid = [[existing_chrome objectAtIndex:0] processIdentifier];
642 AppShimController controller;
643 base::MessageLoopForUI main_message_loop;
644 main_message_loop.set_thread_name("MainThread");
645 base::PlatformThread::SetName("CrAppShimMain");
647 // In tests, launching Chrome does nothing, and we won't get a ping response,
648 // so just assume the socket exists.
650 !base::CommandLine::ForCurrentProcess()->HasSwitch(
651 app_mode::kLaunchedForTest)) {
652 // Launch Chrome if it isn't already running.
653 ProcessSerialNumber psn;
654 base::CommandLine command_line(base::CommandLine::NO_PROGRAM);
655 command_line.AppendSwitch(switches::kSilentLaunch);
657 // If the shim is the app launcher, pass --show-app-list when starting a new
658 // Chrome process to inform startup codepaths and load the correct profile.
659 if (info->app_mode_id == app_mode::kAppListModeId) {
660 command_line.AppendSwitch(switches::kShowAppList);
662 command_line.AppendSwitchPath(switches::kProfileDirectory,
667 base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(),
674 base::Callback<void(bool)> on_ping_chrome_reply =
675 base::Bind(&AppShimController::OnPingChromeReply,
676 base::Unretained(&controller));
678 // This code abuses the fact that Apple Events sent before the process is
679 // fully initialized don't receive a reply until its run loop starts. Once
680 // the reply is received, Chrome will have opened its IPC port, guaranteed.
681 [ReplyEventHandler pingProcess:psn
682 andCall:on_ping_chrome_reply];
684 main_message_loop.PostDelayedTask(
686 base::Bind(&AppShimController::OnPingChromeTimeout,
687 base::Unretained(&controller)),
688 base::TimeDelta::FromSeconds(kPingChromeTimeoutSeconds));
690 // Chrome already running. Proceed to init. This could still fail if Chrome
691 // is still starting up or shutting down, but the process will exit quickly,
692 // which is preferable to waiting for the Apple Event to timeout after one
694 main_message_loop.PostTask(
696 base::Bind(&AppShimController::Init,
697 base::Unretained(&controller)));
700 main_message_loop.Run();