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 "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/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/message_loop/message_loop.h"
26 #include "base/path_service.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 "grit/generated_resources.h"
35 #include "ipc/ipc_channel_proxy.h"
36 #include "ipc/ipc_listener.h"
37 #include "ipc/ipc_message.h"
38 #include "ui/base/resource/resource_bundle.h"
39 #include "ui/base/l10n/l10n_util.h"
41 // Replicate specific 10.7 SDK declarations for building with prior SDKs.
42 #if !defined(MAC_OS_X_VERSION_10_7) || \
43 MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_7
45 @interface NSApplication (LionSDKDeclarations)
46 - (void)disableRelaunchOnLogin;
49 #endif // MAC_OS_X_VERSION_10_7
53 const app_mode::ChromeAppModeInfo* g_info;
54 base::Thread* g_io_thread = NULL;
58 class AppShimController;
60 // An application delegate to catch user interactions and send the appropriate
61 // IPC messages to Chrome.
62 @interface AppShimDelegate : NSObject<NSApplicationDelegate> {
64 AppShimController* appShimController_; // Weak, initially NULL.
66 BOOL terminateRequested_;
67 std::vector<base::FilePath> filesToOpenAtStartup_;
70 // The controller is initially NULL. Setting it indicates to the delegate that
71 // the controller has finished initialization.
72 - (void)setController:(AppShimController*)controller;
74 // Gets files that were queued because the controller was not ready.
75 // Returns whether any FilePaths were added to |out|.
76 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out;
78 // If the controller is ready, this sends a FocusApp with the files to open.
79 // Otherwise, this adds the files to |filesToOpenAtStartup_|.
80 // Takes an array of NSString*.
81 - (void)openFiles:(NSArray*)filename;
83 // Terminate immediately. This is necessary as we override terminate: to send
89 // The AppShimController is responsible for communication with the main Chrome
90 // process, and generally controls the lifetime of the app shim process.
91 class AppShimController : public IPC::Listener {
94 virtual ~AppShimController();
96 // Called when the main Chrome process responds to the Apple Event ping that
97 // was sent, or when the ping fails (if |success| is false).
98 void OnPingChromeReply(bool success);
100 // Connects to Chrome and sends a LaunchApp message.
103 // Builds main menu bar items.
106 void SendSetAppHidden(bool hidden);
110 // Called when the app is activated, e.g. by clicking on it in the dock, by
111 // dropping a file on the dock icon, or by Cmd+Tabbing to it.
112 // Returns whether the message was sent.
113 bool SendFocusApp(apps::AppShimFocusType focus_type,
114 const std::vector<base::FilePath>& files);
117 // IPC::Listener implemetation.
118 virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
119 virtual void OnChannelError() OVERRIDE;
121 // If Chrome failed to launch the app, |success| will be false and the app
122 // shim process should die.
123 void OnLaunchAppDone(apps::AppShimLaunchResult result);
128 // Requests user attention.
129 void OnRequestUserAttention();
131 // Terminates the app shim process.
134 IPC::ChannelProxy* channel_;
135 base::scoped_nsobject<AppShimDelegate> delegate_;
136 bool launch_app_done_;
138 DISALLOW_COPY_AND_ASSIGN(AppShimController);
141 AppShimController::AppShimController()
143 delegate_([[AppShimDelegate alloc] init]),
144 launch_app_done_(false) {
145 // Since AppShimController is created before the main message loop starts,
146 // NSApp will not be set, so use sharedApplication.
147 [[NSApplication sharedApplication] setDelegate:delegate_];
150 AppShimController::~AppShimController() {
151 // Un-set the delegate since NSApplication does not retain it.
152 [NSApp setDelegate:nil];
155 void AppShimController::OnPingChromeReply(bool success) {
157 [NSApp terminate:nil];
164 void AppShimController::Init() {
169 // Chrome will relaunch shims when relaunching apps.
170 if (base::mac::IsOSLionOrLater())
171 [NSApp disableRelaunchOnLogin];
173 // The user_data_dir for shims actually contains the app_data_path.
174 // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
175 base::FilePath user_data_dir =
176 g_info->user_data_dir.DirName().DirName().DirName();
177 CHECK(!user_data_dir.empty());
179 base::FilePath socket_path =
180 user_data_dir.Append(app_mode::kAppShimSocketName);
181 IPC::ChannelHandle handle(socket_path.value());
182 channel_ = new IPC::ChannelProxy(handle, IPC::Channel::MODE_NAMED_CLIENT,
183 this, g_io_thread->message_loop_proxy().get());
185 bool launched_by_chrome =
186 CommandLine::ForCurrentProcess()->HasSwitch(
187 app_mode::kLaunchedByChromeProcessId);
188 apps::AppShimLaunchType launch_type = launched_by_chrome ?
189 apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL;
191 [delegate_ setController:this];
193 std::vector<base::FilePath> files;
194 [delegate_ getFilesToOpenAtStartup:&files];
196 channel_->Send(new AppShimHostMsg_LaunchApp(
197 g_info->profile_dir, g_info->app_mode_id, launch_type, files));
200 void AppShimController::SetUpMenu() {
201 NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name);
203 // Create a main menu since [NSApp mainMenu] is nil.
204 base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]);
206 // The title of the first item is replaced by OSX with the name of the app and
207 // bold styling. Create a dummy item for this and make it hidden.
208 NSMenuItem* dummy_item = [main_menu addItemWithTitle:title
211 base::scoped_nsobject<NSMenu> dummy_submenu(
212 [[NSMenu alloc] initWithTitle:title]);
213 [dummy_item setSubmenu:dummy_submenu];
214 [dummy_item setHidden:YES];
216 // Construct an unbolded app menu, to match how it appears in the Chrome menu
217 // bar when the app is focused.
218 NSMenuItem* item = [main_menu addItemWithTitle:title
221 base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]);
222 [item setSubmenu:submenu];
225 NSString* quit_localized_string =
226 l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name);
227 [submenu addItemWithTitle:quit_localized_string
228 action:@selector(terminate:)
231 // Add File, Edit, and Window menus. These are just here to make the
232 // transition smoother, i.e. from another application to the shim then to
234 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC)
237 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC)
240 [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC)
244 [NSApp setMainMenu:main_menu];
247 void AppShimController::SendQuitApp() {
248 channel_->Send(new AppShimHostMsg_QuitApp);
251 bool AppShimController::OnMessageReceived(const IPC::Message& message) {
253 IPC_BEGIN_MESSAGE_MAP(AppShimController, message)
254 IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone)
255 IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide)
256 IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention)
257 IPC_MESSAGE_UNHANDLED(handled = false)
258 IPC_END_MESSAGE_MAP()
263 void AppShimController::OnChannelError() {
267 void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) {
268 if (result != apps::APP_SHIM_LAUNCH_SUCCESS) {
273 std::vector<base::FilePath> files;
274 if ([delegate_ getFilesToOpenAtStartup:&files])
275 SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files);
277 launch_app_done_ = true;
280 void AppShimController::OnHide() {
284 void AppShimController::OnRequestUserAttention() {
285 [NSApp requestUserAttention:NSInformationalRequest];
288 void AppShimController::Close() {
289 [delegate_ terminateNow];
292 bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type,
293 const std::vector<base::FilePath>& files) {
294 if (launch_app_done_) {
295 channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files));
302 void AppShimController::SendSetAppHidden(bool hidden) {
303 channel_->Send(new AppShimHostMsg_SetAppHidden(hidden));
306 @implementation AppShimDelegate
308 - (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out {
309 if (filesToOpenAtStartup_.empty())
312 out->insert(out->end(),
313 filesToOpenAtStartup_.begin(),
314 filesToOpenAtStartup_.end());
315 filesToOpenAtStartup_.clear();
319 - (void)setController:(AppShimController*)controller {
320 appShimController_ = controller;
323 - (void)openFiles:(NSArray*)filenames {
324 std::vector<base::FilePath> filePaths;
325 for (NSString* filename in filenames)
326 filePaths.push_back(base::mac::NSStringToFilePath(filename));
328 // If the AppShimController is ready, try to send a FocusApp. If that fails,
329 // (e.g. if launching has not finished), enqueue the files.
330 if (appShimController_ &&
331 appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES,
336 filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(),
341 - (BOOL)application:(NSApplication*)app
342 openFile:(NSString*)filename {
343 [self openFiles:@[filename]];
347 - (void)application:(NSApplication*)app
348 openFiles:(NSArray*)filenames {
349 [self openFiles:filenames];
350 [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
353 - (BOOL)applicationOpenUntitledFile:(NSApplication*)app {
354 if (appShimController_) {
355 return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN,
356 std::vector<base::FilePath>());
362 - (void)applicationWillBecomeActive:(NSNotification*)notification {
363 if (appShimController_) {
364 appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL,
365 std::vector<base::FilePath>());
369 - (NSApplicationTerminateReply)
370 applicationShouldTerminate:(NSApplication*)sender {
371 if (terminateNow_ || !appShimController_)
372 return NSTerminateNow;
374 appShimController_->SendQuitApp();
375 // Wait for the channel to close before terminating.
376 terminateRequested_ = YES;
377 return NSTerminateLater;
380 - (void)applicationWillHide:(NSNotification*)notification {
381 if (appShimController_)
382 appShimController_->SendSetAppHidden(true);
385 - (void)applicationWillUnhide:(NSNotification*)notification {
386 if (appShimController_)
387 appShimController_->SendSetAppHidden(false);
390 - (void)terminateNow {
391 if (terminateRequested_) {
392 [NSApp replyToApplicationShouldTerminate:NSTerminateNow];
397 [NSApp terminate:nil];
402 //-----------------------------------------------------------------------------
404 // A ReplyEventHandler is a helper class to send an Apple Event to a process
405 // and call a callback when the reply returns.
407 // This is used to 'ping' the main Chrome process -- once Chrome has sent back
408 // an Apple Event reply, it's guaranteed that it has opened the IPC channel
409 // that the app shim will connect to.
410 @interface ReplyEventHandler : NSObject {
411 base::Callback<void(bool)> onReply_;
414 // Sends an Apple Event to the process identified by |psn|, and calls |replyFn|
415 // when the reply is received. Internally this creates a ReplyEventHandler,
416 // which will delete itself once the reply event has been received.
417 + (void)pingProcess:(const ProcessSerialNumber&)psn
418 andCall:(base::Callback<void(bool)>)replyFn;
421 @interface ReplyEventHandler (PrivateMethods)
422 // Initialise the reply event handler. Doesn't register any handlers until
423 // |-pingProcess:| is called. |replyFn| is the function to be called when the
424 // Apple Event reply arrives.
425 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn;
427 // Sends an Apple Event ping to the process identified by |psn| and registers
428 // to listen for a reply.
429 - (void)pingProcess:(const ProcessSerialNumber&)psn;
431 // Called when a response is received from the target process for the ping sent
432 // by |-pingProcess:|.
433 - (void)message:(NSAppleEventDescriptor*)event
434 withReply:(NSAppleEventDescriptor*)reply;
436 // Calls |onReply_|, passing it |success| to specify whether the ping was
438 - (void)closeWithSuccess:(bool)success;
441 @implementation ReplyEventHandler
442 + (void)pingProcess:(const ProcessSerialNumber&)psn
443 andCall:(base::Callback<void(bool)>)replyFn {
444 // The object will release itself when the reply arrives, or possibly earlier
445 // if an unrecoverable error occurs.
446 ReplyEventHandler* handler =
447 [[ReplyEventHandler alloc] initWithCallback:replyFn];
448 [handler pingProcess:psn];
452 @implementation ReplyEventHandler (PrivateMethods)
453 - (id)initWithCallback:(base::Callback<void(bool)>)replyFn {
454 if ((self = [super init])) {
460 - (void)pingProcess:(const ProcessSerialNumber&)psn {
461 // Register the reply listener.
462 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
463 [em setEventHandler:self
464 andSelector:@selector(message:withReply:)
467 // Craft the Apple Event to send.
468 NSAppleEventDescriptor* target = [NSAppleEventDescriptor
469 descriptorWithDescriptorType:typeProcessSerialNumber
472 NSAppleEventDescriptor* initial_event =
473 [NSAppleEventDescriptor
474 appleEventWithEventClass:app_mode::kAEChromeAppClass
475 eventID:app_mode::kAEChromeAppPing
476 targetDescriptor:target
477 returnID:kAutoGenerateReturnID
478 transactionID:kAnyTransactionID];
480 // TODO(jeremya): if we don't care about the contents of the reply, can we
481 // pass NULL for the reply event parameter?
482 OSStatus status = AESendMessage(
483 [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout);
484 if (status != noErr) {
485 OSSTATUS_LOG(ERROR, status) << "AESendMessage";
486 [self closeWithSuccess:false];
490 - (void)message:(NSAppleEventDescriptor*)event
491 withReply:(NSAppleEventDescriptor*)reply {
492 [self closeWithSuccess:true];
495 - (void)closeWithSuccess:(bool)success {
496 onReply_.Run(success);
497 NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager];
498 [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr'];
503 //-----------------------------------------------------------------------------
507 // |ChromeAppModeStart()| is the point of entry into the framework from the app
509 __attribute__((visibility("default")))
510 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info);
514 int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) {
515 CommandLine::Init(info->argc, info->argv);
517 base::mac::ScopedNSAutoreleasePool scoped_pool;
518 base::AtExitManager exit_manager;
519 chrome::RegisterPathProvider();
521 if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) {
522 RAW_LOG(ERROR, "App Mode Loader too old.");
525 if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) {
526 RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut.");
532 // Set bundle paths. This loads the bundles.
533 base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path);
534 base::mac::SetOverrideFrameworkBundlePath(
535 g_info->chrome_versioned_path.Append(chrome::kFrameworkName));
537 // Calculate the preferred locale used by Chrome.
538 // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls
539 // [base::mac::OuterBundle() preferredLocalizations] which gets localizations
540 // from the bundle of the running app (i.e. it is equivalent to
541 // [[NSBundle mainBundle] preferredLocalizations]) instead of the target
543 NSArray* preferred_languages = [NSLocale preferredLanguages];
544 NSArray* supported_languages = [base::mac::OuterBundle() localizations];
545 std::string preferred_localization;
546 for (NSString* language in preferred_languages) {
547 if ([supported_languages containsObject:language]) {
548 preferred_localization = base::SysNSStringToUTF8(language);
552 std::string locale = l10n_util::NormalizeLocale(
553 l10n_util::GetApplicationLocale(preferred_localization));
555 // Load localized strings.
556 ResourceBundle::InitSharedInstanceLocaleOnly(locale, NULL);
558 // Launch the IO thread.
559 base::Thread::Options io_thread_options;
560 io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO;
561 base::Thread *io_thread = new base::Thread("CrAppShimIO");
562 io_thread->StartWithOptions(io_thread_options);
563 g_io_thread = io_thread;
565 // Find already running instances of Chrome.
567 std::string chrome_process_id = CommandLine::ForCurrentProcess()->
568 GetSwitchValueASCII(app_mode::kLaunchedByChromeProcessId);
569 if (!chrome_process_id.empty()) {
570 if (!base::StringToInt(chrome_process_id, &pid))
571 LOG(FATAL) << "Invalid PID: " << chrome_process_id;
573 NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier];
574 NSArray* existing_chrome = [NSRunningApplication
575 runningApplicationsWithBundleIdentifier:chrome_bundle_id];
576 if ([existing_chrome count] > 0)
577 pid = [[existing_chrome objectAtIndex:0] processIdentifier];
580 // Launch Chrome if it isn't already running.
581 ProcessSerialNumber psn;
583 OSStatus status = GetProcessForPID(pid, &psn);
588 CommandLine command_line(CommandLine::NO_PROGRAM);
589 command_line.AppendSwitch(switches::kSilentLaunch);
590 command_line.AppendSwitchPath(switches::kProfileDirectory,
593 base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(),
601 AppShimController controller;
602 base::Callback<void(bool)> on_ping_chrome_reply =
603 base::Bind(&AppShimController::OnPingChromeReply,
604 base::Unretained(&controller));
606 // This code abuses the fact that Apple Events sent before the process is
607 // fully initialized don't receive a reply until its run loop starts. Once
608 // the reply is received, Chrome will have opened its IPC port, guaranteed.
609 [ReplyEventHandler pingProcess:psn
610 andCall:on_ping_chrome_reply];
612 base::MessageLoopForUI main_message_loop;
613 main_message_loop.set_thread_name("MainThread");
614 base::PlatformThread::SetName("CrAppShimMain");
615 main_message_loop.Run();