diff options
author | tapted <tapted@chromium.org> | 2014-09-24 16:50:50 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2014-09-24 23:51:09 +0000 |
commit | 63829f711468110d04c7464acf92c3e7ab3c13d7 (patch) | |
tree | f57c59b72f62d1ae05d3fbbd11963d69deeef6aa /chrome/app_shim | |
parent | 18112c74cee064411a29cfdc3f3464b653b4a67a (diff) | |
download | chromium_src-63829f711468110d04c7464acf92c3e7ab3c13d7.zip chromium_src-63829f711468110d04c7464acf92c3e7ab3c13d7.tar.gz chromium_src-63829f711468110d04c7464acf92c3e7ab3c13d7.tar.bz2 |
Mac: Give app_shim code a nicer home
App Shims are small .app bundles for Mac, which run in their own
process to provide packaged apps with an OSX Dock icon, using IPC to
communicate with the browser process.
Most of the app_shim code currently lives in src/apps/app_shim, but app
shims are very much a browser feature, so it doesn't belong there. It's
causing DEPS problems for athena efforts.
This CL moves most of apps/app_shim to chrome/browser/apps/app_shim
(c/b/apps didn't exist until a few months after src/apps/app_shim).
The shim itself (i.e. chrome_main_app_mode_mac.mm) doesn't run in the
browser process - it has its own, shim process. Those parts are moved to
a new folder: src/chrome/app_shim, consolidating files currently spread
across src/apps/app_shim and src/chrome/app.
app_shim_launch.h and app_shim_messages.h are used in both the shim
process and the browser process. They're moved to chrome/common/mac.
Summary of changes:
{apps => chrome/browser/apps}/app_shim/*
except
{apps => chrome}/app_shim/chrome_main_app_mode_mac.mm
{apps/app_shim => chrome/common/mac}/app_shim_launch.h
{apps/app_shim => chrome/common/mac}/app_shim_messages.h
app_shim.gypi renamed to browser_app_shim.gypi
chrome/app_shim/app_shim.gypi added
and
chrome/{app => app_shim}/app_mode-Info.plist
chrome/{app => app_shim}/app_mode_loader_mac.mm
- apps/app_shim/DEPS now just generated_resources.h
- 'app_mode_app' target moved from chrome.gyp to app_shim.gypi
BUG=266705
Review URL: https://codereview.chromium.org/585123004
Cr-Commit-Position: refs/heads/master@{#296576}
Diffstat (limited to 'chrome/app_shim')
-rw-r--r-- | chrome/app_shim/DEPS | 3 | ||||
-rw-r--r-- | chrome/app_shim/OWNERS | 2 | ||||
-rw-r--r-- | chrome/app_shim/app_mode-Info.plist | 34 | ||||
-rw-r--r-- | chrome/app_shim/app_mode_loader_mac.mm | 206 | ||||
-rw-r--r-- | chrome/app_shim/app_shim.gypi | 82 | ||||
-rw-r--r-- | chrome/app_shim/chrome_main_app_mode_mac.mm | 693 |
6 files changed, 1020 insertions, 0 deletions
diff --git a/chrome/app_shim/DEPS b/chrome/app_shim/DEPS new file mode 100644 index 0000000..4cb2de8 --- /dev/null +++ b/chrome/app_shim/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+chrome/grit/generated_resources.h", +] diff --git a/chrome/app_shim/OWNERS b/chrome/app_shim/OWNERS new file mode 100644 index 0000000..7f10d7c --- /dev/null +++ b/chrome/app_shim/OWNERS @@ -0,0 +1,2 @@ +tapted@chromium.org +jackhou@chromium.org diff --git a/chrome/app_shim/app_mode-Info.plist b/chrome/app_shim/app_mode-Info.plist new file mode 100644 index 0000000..1ca1b95 --- /dev/null +++ b/chrome/app_shim/app_mode-Info.plist @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIconFile</key> + <string>app.icns</string> + <key>CFBundleIdentifier</key> + <string>${APP_MODE_APP_BUNDLE_ID}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>@APP_MODE_SHORTCUT_ID@</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CrAppModeShortcutID</key> + <string>@APP_MODE_SHORTCUT_ID@</string> + <key>CrAppModeShortcutName</key> + <string>@APP_MODE_SHORTCUT_NAME@</string> + <key>CrAppModeShortcutURL</key> + <string>@APP_MODE_SHORTCUT_URL@</string> + <key>CrBundleIdentifier</key> + <string>@APP_MODE_BROWSER_BUNDLE_ID@</string> + <key>LSMinimumSystemVersion</key> + <string>${MACOSX_DEPLOYMENT_TARGET}.0</string> + <key>NSAppleScriptEnabled</key> + <true/> +</dict> +</plist> diff --git a/chrome/app_shim/app_mode_loader_mac.mm b/chrome/app_shim/app_mode_loader_mac.mm new file mode 100644 index 0000000..ba9da17 --- /dev/null +++ b/chrome/app_shim/app_mode_loader_mac.mm @@ -0,0 +1,206 @@ +// Copyright (c) 2012 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, shortcuts can't have command-line arguments. Instead, produce small +// app bundles which locate the Chromium framework and load it, passing the +// appropriate data. This is the code for such an app bundle. It should be kept +// minimal and do as little work as possible (with as much work done on +// framework side as possible). + +#include <dlfcn.h> + +#import <Cocoa/Cocoa.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/foundation_util.h" +#include "base/mac/launch_services_util.h" +#include "base/mac/scoped_nsautorelease_pool.h" +#include "base/process/launch.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/sys_string_conversions.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/chrome_switches.h" +#import "chrome/common/mac/app_mode_chrome_locator.h" +#include "chrome/common/mac/app_mode_common.h" + +namespace { + +typedef int (*StartFun)(const app_mode::ChromeAppModeInfo*); + +int LoadFrameworkAndStart(app_mode::ChromeAppModeInfo* info) { + using base::SysNSStringToUTF8; + using base::SysNSStringToUTF16; + using base::mac::CFToNSCast; + using base::mac::CFCastStrict; + using base::mac::NSToCFCast; + + base::mac::ScopedNSAutoreleasePool scoped_pool; + + // Get the current main bundle, i.e., that of the app loader that's running. + NSBundle* app_bundle = [NSBundle mainBundle]; + CHECK(app_bundle) << "couldn't get loader bundle"; + + // ** 1: Get path to outer Chrome bundle. + // Get the bundle ID of the browser that created this app bundle. + NSString* cr_bundle_id = base::mac::ObjCCast<NSString>( + [app_bundle objectForInfoDictionaryKey:app_mode::kBrowserBundleIDKey]); + CHECK(cr_bundle_id) << "couldn't get browser bundle ID"; + + // First check if Chrome exists at the last known location. + base::FilePath cr_bundle_path; + NSString* cr_bundle_path_ns = + [CFToNSCast(CFCastStrict<CFStringRef>(CFPreferencesCopyAppValue( + NSToCFCast(app_mode::kLastRunAppBundlePathPrefsKey), + NSToCFCast(cr_bundle_id)))) autorelease]; + cr_bundle_path = base::mac::NSStringToFilePath(cr_bundle_path_ns); + bool found_bundle = + !cr_bundle_path.empty() && base::DirectoryExists(cr_bundle_path); + + if (!found_bundle) { + // If no such bundle path exists, try to search by bundle ID. + if (!app_mode::FindBundleById(cr_bundle_id, &cr_bundle_path)) { + // TODO(jeremy): Display UI to allow user to manually locate the Chrome + // bundle. + LOG(FATAL) << "Failed to locate bundle by identifier"; + } + } + + // ** 2: Read the running Chrome version. + // The user_data_dir for shims actually contains the app_data_path. + // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/ + base::FilePath app_data_dir = base::mac::NSStringToFilePath([app_bundle + objectForInfoDictionaryKey:app_mode::kCrAppModeUserDataDirKey]); + base::FilePath user_data_dir = app_data_dir.DirName().DirName().DirName(); + CHECK(!user_data_dir.empty()); + + // If the version file does not exist, |cr_version_str| will be empty and + // app_mode::GetChromeBundleInfo will default to the latest version. + base::FilePath cr_version_str; + base::ReadSymbolicLink( + user_data_dir.Append(app_mode::kRunningChromeVersionSymlinkName), + &cr_version_str); + + // If the version file does exist, it may have been left by a crashed Chrome + // process. Ensure the process is still running. + if (!cr_version_str.empty()) { + NSArray* existing_chrome = [NSRunningApplication + runningApplicationsWithBundleIdentifier:cr_bundle_id]; + if ([existing_chrome count] == 0) + cr_version_str.clear(); + } + + // ** 3: Read information from the Chrome bundle. + base::FilePath executable_path; + base::FilePath version_path; + base::FilePath framework_shlib_path; + if (!app_mode::GetChromeBundleInfo(cr_bundle_path, + cr_version_str.value(), + &executable_path, + &version_path, + &framework_shlib_path)) { + LOG(FATAL) << "Couldn't ready Chrome bundle info"; + } + base::FilePath app_mode_bundle_path = + base::mac::NSStringToFilePath([app_bundle bundlePath]); + + // ** 4: Fill in ChromeAppModeInfo. + info->chrome_outer_bundle_path = cr_bundle_path; + info->chrome_versioned_path = version_path; + info->app_mode_bundle_path = app_mode_bundle_path; + + // Read information about the this app shortcut from the Info.plist. + // Don't check for null-ness on optional items. + NSDictionary* info_plist = [app_bundle infoDictionary]; + CHECK(info_plist) << "couldn't get loader Info.plist"; + + info->app_mode_id = SysNSStringToUTF8( + [info_plist objectForKey:app_mode::kCrAppModeShortcutIDKey]); + CHECK(info->app_mode_id.size()) << "couldn't get app shortcut ID"; + + info->app_mode_name = SysNSStringToUTF16( + [info_plist objectForKey:app_mode::kCrAppModeShortcutNameKey]); + + info->app_mode_url = SysNSStringToUTF8( + [info_plist objectForKey:app_mode::kCrAppModeShortcutURLKey]); + + info->user_data_dir = base::mac::NSStringToFilePath( + [info_plist objectForKey:app_mode::kCrAppModeUserDataDirKey]); + + info->profile_dir = base::mac::NSStringToFilePath( + [info_plist objectForKey:app_mode::kCrAppModeProfileDirKey]); + + // ** 5: Open the framework. + StartFun ChromeAppModeStart = NULL; + void* cr_dylib = dlopen(framework_shlib_path.value().c_str(), RTLD_LAZY); + if (cr_dylib) { + // Find the entry point. + ChromeAppModeStart = (StartFun)dlsym(cr_dylib, "ChromeAppModeStart"); + if (!ChromeAppModeStart) + LOG(ERROR) << "Couldn't get entry point: " << dlerror(); + } else { + LOG(ERROR) << "Couldn't load framework: " << dlerror(); + } + + if (ChromeAppModeStart) + return ChromeAppModeStart(info); + + LOG(ERROR) << "Loading Chrome failed, launching Chrome with command line"; + CommandLine command_line(executable_path); + // The user_data_dir from the plist is actually the app data dir. + command_line.AppendSwitchPath( + switches::kUserDataDir, + info->user_data_dir.DirName().DirName().DirName()); + if (CommandLine::ForCurrentProcess()->HasSwitch( + app_mode::kLaunchedByChromeProcessId) || + info->app_mode_id == app_mode::kAppListModeId) { + // Pass --app-shim-error to have Chrome rebuild this shim. + // If Chrome has rebuilt this shim once already, then rebuilding doesn't fix + // the problem, so don't try again. + if (!CommandLine::ForCurrentProcess()->HasSwitch( + app_mode::kLaunchedAfterRebuild)) { + command_line.AppendSwitchPath(app_mode::kAppShimError, + app_mode_bundle_path); + } + } else { + // If the shim was launched directly (instead of by Chrome), first ask + // Chrome to launch the app. Chrome will launch the shim again, the same + // error will occur and be handled above. This approach allows the app to be + // started without blocking on fixing the shim and guarantees that the + // profile is loaded when Chrome receives --app-shim-error. + command_line.AppendSwitchPath(switches::kProfileDirectory, + info->profile_dir); + command_line.AppendSwitchASCII(switches::kAppId, info->app_mode_id); + } + // Launch the executable directly since base::mac::OpenApplicationWithPath + // uses LSOpenApplication which doesn't pass command line arguments if the + // application is already running. + if (!base::LaunchProcess(command_line, base::LaunchOptions(), NULL)) { + LOG(ERROR) << "Could not launch Chrome: " + << command_line.GetCommandLineString(); + return 1; + } + + return 0; +} + +} // namespace + +__attribute__((visibility("default"))) +int main(int argc, char** argv) { + CommandLine::Init(argc, argv); + app_mode::ChromeAppModeInfo info; + + // Hard coded info parameters. + info.major_version = app_mode::kCurrentChromeAppModeInfoMajorVersion; + info.minor_version = app_mode::kCurrentChromeAppModeInfoMinorVersion; + info.argc = argc; + info.argv = argv; + + // Exit instead of returning to avoid the the removal of |main()| from stack + // backtraces under tail call optimization. + exit(LoadFrameworkAndStart(&info)); +} diff --git a/chrome/app_shim/app_shim.gypi b/chrome/app_shim/app_shim.gypi new file mode 100644 index 0000000..7e6d04db --- /dev/null +++ b/chrome/app_shim/app_shim.gypi @@ -0,0 +1,82 @@ +# Copyright 2014 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. + +{ + 'targets': [ + { + # This is the part of the shim process compiled into Chrome. It runs only + # in the shim process, after the shim finds and loads the Chrome + # Framework bundle. + 'target_name': 'app_shim', + 'type': 'static_library', + 'dependencies': [ + # Since app_shim and browser depend on each other, we omit the + # dependency on browser here. + '../chrome/chrome_resources.gyp:chrome_strings', + 'app_mode_app_support', + ], + 'sources': [ + 'chrome_main_app_mode_mac.mm', + ], + 'include_dirs': [ + '<(INTERMEDIATE_DIR)', + '../..', + ], + }, # target app_shim + { + # This produces the template for app mode loader bundles. It's a template + # in the sense that parts of it need to be "filled in" by Chrome before it + # can be executed. + 'target_name': 'app_mode_app', + 'type': 'executable', + 'mac_bundle' : 1, + 'variables': { + 'enable_wexit_time_destructors': 1, + 'mac_real_dsym': 1, + }, + 'product_name': 'app_mode_loader', + 'dependencies': [ + 'app_mode_app_support', + 'infoplist_strings_tool', + ], + 'sources': [ + 'app_mode_loader_mac.mm', + 'app_mode-Info.plist', + ], + 'include_dirs': [ + '../..', + ], + 'link_settings': { + 'libraries': [ + '$(SDKROOT)/System/Library/Frameworks/CoreFoundation.framework', + '$(SDKROOT)/System/Library/Frameworks/Foundation.framework', + ], + }, + 'mac_bundle_resources!': [ + 'app_shim/app_mode-Info.plist', + ], + 'mac_bundle_resources/': [ + ['exclude', '.*'], + ], + 'xcode_settings': { + 'INFOPLIST_FILE': 'app_shim/app_mode-Info.plist', + 'APP_MODE_APP_BUNDLE_ID': '<(mac_bundle_id).app.@APP_MODE_SHORTCUT_ID@', + }, + 'postbuilds' : [ + { + # Modify the Info.plist as needed. The script explains why this + # is needed. This is also done in the chrome and chrome_dll + # targets. In this case, --breakpad=0, --keystone=0, and --scm=0 + # are used because Breakpad, Keystone, and SCM keys are + # never placed into the app mode loader. + 'postbuild_name': 'Tweak Info.plist', + 'action': ['<(tweak_info_plist_path)', + '--breakpad=0', + '--keystone=0', + '--scm=0'], + }, + ], + }, # target app_mode_app + ], # targets +} diff --git a/chrome/app_shim/chrome_main_app_mode_mac.mm b/chrome/app_shim/chrome_main_app_mode_mac.mm new file mode 100644 index 0000000..e9cf194 --- /dev/null +++ b/chrome/app_shim/chrome_main_app_mode_mac.mm @@ -0,0 +1,693 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// On Mac, one can't make shortcuts with command-line arguments. Instead, we +// produce small app bundles which locate the Chromium framework and load it, +// passing the appropriate data. This is the entry point into the framework for +// those app bundles. + +#import <Cocoa/Cocoa.h> +#include <vector> + +#include "base/at_exit.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" +#include "base/logging.h" +#include "base/mac/bundle_locations.h" +#include "base/mac/foundation_util.h" +#include "base/mac/launch_services_util.h" +#include "base/mac/mac_logging.h" +#include "base/mac/mac_util.h" +#include "base/mac/scoped_nsautorelease_pool.h" +#include "base/mac/scoped_nsobject.h" +#include "base/mac/sdk_forward_declarations.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/sys_string_conversions.h" +#include "base/threading/thread.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/mac/app_mode_common.h" +#include "chrome/common/mac/app_shim_messages.h" +#include "chrome/grit/generated_resources.h" +#include "ipc/ipc_channel_proxy.h" +#include "ipc/ipc_listener.h" +#include "ipc/ipc_message.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/resource/resource_bundle.h" + +namespace { + +// Timeout in seconds to wait for a reply for the initial Apple Event. Note that +// kAEDefaultTimeout on Mac is "about one minute" according to Apple's +// documentation, but is no longer supported for asynchronous Apple Events. +const int kPingChromeTimeoutSeconds = 60; + +const app_mode::ChromeAppModeInfo* g_info; +base::Thread* g_io_thread = NULL; + +} // namespace + +class AppShimController; + +// An application delegate to catch user interactions and send the appropriate +// IPC messages to Chrome. +@interface AppShimDelegate : NSObject<NSApplicationDelegate> { + @private + AppShimController* appShimController_; // Weak, initially NULL. + BOOL terminateNow_; + BOOL terminateRequested_; + std::vector<base::FilePath> filesToOpenAtStartup_; +} + +// The controller is initially NULL. Setting it indicates to the delegate that +// the controller has finished initialization. +- (void)setController:(AppShimController*)controller; + +// Gets files that were queued because the controller was not ready. +// Returns whether any FilePaths were added to |out|. +- (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out; + +// If the controller is ready, this sends a FocusApp with the files to open. +// Otherwise, this adds the files to |filesToOpenAtStartup_|. +// Takes an array of NSString*. +- (void)openFiles:(NSArray*)filename; + +// Terminate immediately. This is necessary as we override terminate: to send +// a QuitApp message. +- (void)terminateNow; + +@end + +// The AppShimController is responsible for communication with the main Chrome +// process, and generally controls the lifetime of the app shim process. +class AppShimController : public IPC::Listener { + public: + AppShimController(); + virtual ~AppShimController(); + + // Called when the main Chrome process responds to the Apple Event ping that + // was sent, or when the ping fails (if |success| is false). + void OnPingChromeReply(bool success); + + // Called |kPingChromeTimeoutSeconds| after startup, to allow a timeout on the + // ping event to be detected. + void OnPingChromeTimeout(); + + // Connects to Chrome and sends a LaunchApp message. + void Init(); + + // Create a channel from |socket_path| and send a LaunchApp message. + void CreateChannelAndSendLaunchApp(const base::FilePath& socket_path); + + // Builds main menu bar items. + void SetUpMenu(); + + void SendSetAppHidden(bool hidden); + + void SendQuitApp(); + + // Called when the app is activated, e.g. by clicking on it in the dock, by + // dropping a file on the dock icon, or by Cmd+Tabbing to it. + // Returns whether the message was sent. + bool SendFocusApp(apps::AppShimFocusType focus_type, + const std::vector<base::FilePath>& files); + + private: + // IPC::Listener implemetation. + virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; + virtual void OnChannelError() OVERRIDE; + + // If Chrome failed to launch the app, |success| will be false and the app + // shim process should die. + void OnLaunchAppDone(apps::AppShimLaunchResult result); + + // Hide this app. + void OnHide(); + + // Requests user attention. + void OnRequestUserAttention(); + void OnSetUserAttention(apps::AppShimAttentionType attention_type); + + // Terminates the app shim process. + void Close(); + + base::FilePath user_data_dir_; + scoped_ptr<IPC::ChannelProxy> channel_; + base::scoped_nsobject<AppShimDelegate> delegate_; + bool launch_app_done_; + bool ping_chrome_reply_received_; + NSInteger attention_request_id_; + + DISALLOW_COPY_AND_ASSIGN(AppShimController); +}; + +AppShimController::AppShimController() + : delegate_([[AppShimDelegate alloc] init]), + launch_app_done_(false), + ping_chrome_reply_received_(false), + attention_request_id_(0) { + // Since AppShimController is created before the main message loop starts, + // NSApp will not be set, so use sharedApplication. + [[NSApplication sharedApplication] setDelegate:delegate_]; +} + +AppShimController::~AppShimController() { + // Un-set the delegate since NSApplication does not retain it. + [[NSApplication sharedApplication] setDelegate:nil]; +} + +void AppShimController::OnPingChromeReply(bool success) { + ping_chrome_reply_received_ = true; + if (!success) { + [NSApp terminate:nil]; + return; + } + + Init(); +} + +void AppShimController::OnPingChromeTimeout() { + if (!ping_chrome_reply_received_) + [NSApp terminate:nil]; +} + +void AppShimController::Init() { + DCHECK(g_io_thread); + + SetUpMenu(); + + // Chrome will relaunch shims when relaunching apps. + if (base::mac::IsOSLionOrLater()) + [NSApp disableRelaunchOnLogin]; + + // The user_data_dir for shims actually contains the app_data_path. + // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/ + user_data_dir_ = g_info->user_data_dir.DirName().DirName().DirName(); + CHECK(!user_data_dir_.empty()); + + base::FilePath symlink_path = + user_data_dir_.Append(app_mode::kAppShimSocketSymlinkName); + + base::FilePath socket_path; + if (!base::ReadSymbolicLink(symlink_path, &socket_path)) { + // The path in the user data dir is not a symlink, try connecting directly. + CreateChannelAndSendLaunchApp(symlink_path); + return; + } + + app_mode::VerifySocketPermissions(socket_path); + + CreateChannelAndSendLaunchApp(socket_path); +} + +void AppShimController::CreateChannelAndSendLaunchApp( + const base::FilePath& socket_path) { + IPC::ChannelHandle handle(socket_path.value()); + channel_ = IPC::ChannelProxy::Create(handle, + IPC::Channel::MODE_NAMED_CLIENT, + this, + g_io_thread->message_loop_proxy().get()); + + bool launched_by_chrome = + CommandLine::ForCurrentProcess()->HasSwitch( + app_mode::kLaunchedByChromeProcessId); + apps::AppShimLaunchType launch_type = launched_by_chrome ? + apps::APP_SHIM_LAUNCH_REGISTER_ONLY : apps::APP_SHIM_LAUNCH_NORMAL; + + [delegate_ setController:this]; + + std::vector<base::FilePath> files; + [delegate_ getFilesToOpenAtStartup:&files]; + + channel_->Send(new AppShimHostMsg_LaunchApp( + g_info->profile_dir, g_info->app_mode_id, launch_type, files)); +} + +void AppShimController::SetUpMenu() { + NSString* title = base::SysUTF16ToNSString(g_info->app_mode_name); + + // Create a main menu since [NSApp mainMenu] is nil. + base::scoped_nsobject<NSMenu> main_menu([[NSMenu alloc] initWithTitle:title]); + + // The title of the first item is replaced by OSX with the name of the app and + // bold styling. Create a dummy item for this and make it hidden. + NSMenuItem* dummy_item = [main_menu addItemWithTitle:title + action:nil + keyEquivalent:@""]; + base::scoped_nsobject<NSMenu> dummy_submenu( + [[NSMenu alloc] initWithTitle:title]); + [dummy_item setSubmenu:dummy_submenu]; + [dummy_item setHidden:YES]; + + // Construct an unbolded app menu, to match how it appears in the Chrome menu + // bar when the app is focused. + NSMenuItem* item = [main_menu addItemWithTitle:title + action:nil + keyEquivalent:@""]; + base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] initWithTitle:title]); + [item setSubmenu:submenu]; + + // Add a quit entry. + NSString* quit_localized_string = + l10n_util::GetNSStringF(IDS_EXIT_MAC, g_info->app_mode_name); + [submenu addItemWithTitle:quit_localized_string + action:@selector(terminate:) + keyEquivalent:@"q"]; + + // Add File, Edit, and Window menus. These are just here to make the + // transition smoother, i.e. from another application to the shim then to + // Chrome. + [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_FILE_MENU_MAC) + action:nil + keyEquivalent:@""]; + [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_EDIT_MENU_MAC) + action:nil + keyEquivalent:@""]; + [main_menu addItemWithTitle:l10n_util::GetNSString(IDS_WINDOW_MENU_MAC) + action:nil + keyEquivalent:@""]; + + [NSApp setMainMenu:main_menu]; +} + +void AppShimController::SendQuitApp() { + channel_->Send(new AppShimHostMsg_QuitApp); +} + +bool AppShimController::OnMessageReceived(const IPC::Message& message) { + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(AppShimController, message) + IPC_MESSAGE_HANDLER(AppShimMsg_LaunchApp_Done, OnLaunchAppDone) + IPC_MESSAGE_HANDLER(AppShimMsg_Hide, OnHide) + IPC_MESSAGE_HANDLER(AppShimMsg_RequestUserAttention, OnRequestUserAttention) + IPC_MESSAGE_HANDLER(AppShimMsg_SetUserAttention, OnSetUserAttention) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + + return handled; +} + +void AppShimController::OnChannelError() { + Close(); +} + +void AppShimController::OnLaunchAppDone(apps::AppShimLaunchResult result) { + if (result != apps::APP_SHIM_LAUNCH_SUCCESS) { + Close(); + return; + } + + std::vector<base::FilePath> files; + if ([delegate_ getFilesToOpenAtStartup:&files]) + SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, files); + + launch_app_done_ = true; +} + +void AppShimController::OnHide() { + [NSApp hide:nil]; +} + +void AppShimController::OnRequestUserAttention() { + OnSetUserAttention(apps::APP_SHIM_ATTENTION_INFORMATIONAL); +} + +void AppShimController::OnSetUserAttention( + apps::AppShimAttentionType attention_type) { + switch (attention_type) { + case apps::APP_SHIM_ATTENTION_CANCEL: + [NSApp cancelUserAttentionRequest:attention_request_id_]; + attention_request_id_ = 0; + break; + case apps::APP_SHIM_ATTENTION_CRITICAL: + attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest]; + break; + case apps::APP_SHIM_ATTENTION_INFORMATIONAL: + attention_request_id_ = + [NSApp requestUserAttention:NSInformationalRequest]; + break; + case apps::APP_SHIM_ATTENTION_NUM_TYPES: + NOTREACHED(); + } +} + +void AppShimController::Close() { + [delegate_ terminateNow]; +} + +bool AppShimController::SendFocusApp(apps::AppShimFocusType focus_type, + const std::vector<base::FilePath>& files) { + if (launch_app_done_) { + channel_->Send(new AppShimHostMsg_FocusApp(focus_type, files)); + return true; + } + + return false; +} + +void AppShimController::SendSetAppHidden(bool hidden) { + channel_->Send(new AppShimHostMsg_SetAppHidden(hidden)); +} + +@implementation AppShimDelegate + +- (BOOL)getFilesToOpenAtStartup:(std::vector<base::FilePath>*)out { + if (filesToOpenAtStartup_.empty()) + return NO; + + out->insert(out->end(), + filesToOpenAtStartup_.begin(), + filesToOpenAtStartup_.end()); + filesToOpenAtStartup_.clear(); + return YES; +} + +- (void)setController:(AppShimController*)controller { + appShimController_ = controller; +} + +- (void)openFiles:(NSArray*)filenames { + std::vector<base::FilePath> filePaths; + for (NSString* filename in filenames) + filePaths.push_back(base::mac::NSStringToFilePath(filename)); + + // If the AppShimController is ready, try to send a FocusApp. If that fails, + // (e.g. if launching has not finished), enqueue the files. + if (appShimController_ && + appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_OPEN_FILES, + filePaths)) { + return; + } + + filesToOpenAtStartup_.insert(filesToOpenAtStartup_.end(), + filePaths.begin(), + filePaths.end()); +} + +- (BOOL)application:(NSApplication*)app + openFile:(NSString*)filename { + [self openFiles:@[filename]]; + return YES; +} + +- (void)application:(NSApplication*)app + openFiles:(NSArray*)filenames { + [self openFiles:filenames]; + [app replyToOpenOrPrint:NSApplicationDelegateReplySuccess]; +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication*)app { + if (appShimController_) { + return appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_REOPEN, + std::vector<base::FilePath>()); + } + + return NO; +} + +- (void)applicationWillBecomeActive:(NSNotification*)notification { + if (appShimController_) { + appShimController_->SendFocusApp(apps::APP_SHIM_FOCUS_NORMAL, + std::vector<base::FilePath>()); + } +} + +- (NSApplicationTerminateReply) + applicationShouldTerminate:(NSApplication*)sender { + if (terminateNow_ || !appShimController_) + return NSTerminateNow; + + appShimController_->SendQuitApp(); + // Wait for the channel to close before terminating. + terminateRequested_ = YES; + return NSTerminateLater; +} + +- (void)applicationWillHide:(NSNotification*)notification { + if (appShimController_) + appShimController_->SendSetAppHidden(true); +} + +- (void)applicationWillUnhide:(NSNotification*)notification { + if (appShimController_) + appShimController_->SendSetAppHidden(false); +} + +- (void)terminateNow { + if (terminateRequested_) { + [NSApp replyToApplicationShouldTerminate:NSTerminateNow]; + return; + } + + terminateNow_ = YES; + [NSApp terminate:nil]; +} + +@end + +//----------------------------------------------------------------------------- + +// A ReplyEventHandler is a helper class to send an Apple Event to a process +// and call a callback when the reply returns. +// +// This is used to 'ping' the main Chrome process -- once Chrome has sent back +// an Apple Event reply, it's guaranteed that it has opened the IPC channel +// that the app shim will connect to. +@interface ReplyEventHandler : NSObject { + base::Callback<void(bool)> onReply_; + AEDesc replyEvent_; +} +// Sends an Apple Event to the process identified by |psn|, and calls |replyFn| +// when the reply is received. Internally this creates a ReplyEventHandler, +// which will delete itself once the reply event has been received. ++ (void)pingProcess:(const ProcessSerialNumber&)psn + andCall:(base::Callback<void(bool)>)replyFn; +@end + +@interface ReplyEventHandler (PrivateMethods) +// Initialise the reply event handler. Doesn't register any handlers until +// |-pingProcess:| is called. |replyFn| is the function to be called when the +// Apple Event reply arrives. +- (id)initWithCallback:(base::Callback<void(bool)>)replyFn; + +// Sends an Apple Event ping to the process identified by |psn| and registers +// to listen for a reply. +- (void)pingProcess:(const ProcessSerialNumber&)psn; + +// Called when a response is received from the target process for the ping sent +// by |-pingProcess:|. +- (void)message:(NSAppleEventDescriptor*)event + withReply:(NSAppleEventDescriptor*)reply; + +// Calls |onReply_|, passing it |success| to specify whether the ping was +// successful. +- (void)closeWithSuccess:(bool)success; +@end + +@implementation ReplyEventHandler ++ (void)pingProcess:(const ProcessSerialNumber&)psn + andCall:(base::Callback<void(bool)>)replyFn { + // The object will release itself when the reply arrives, or possibly earlier + // if an unrecoverable error occurs. + ReplyEventHandler* handler = + [[ReplyEventHandler alloc] initWithCallback:replyFn]; + [handler pingProcess:psn]; +} +@end + +@implementation ReplyEventHandler (PrivateMethods) +- (id)initWithCallback:(base::Callback<void(bool)>)replyFn { + if ((self = [super init])) { + onReply_ = replyFn; + } + return self; +} + +- (void)pingProcess:(const ProcessSerialNumber&)psn { + // Register the reply listener. + NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; + [em setEventHandler:self + andSelector:@selector(message:withReply:) + forEventClass:'aevt' + andEventID:'ansr']; + // Craft the Apple Event to send. + NSAppleEventDescriptor* target = [NSAppleEventDescriptor + descriptorWithDescriptorType:typeProcessSerialNumber + bytes:&psn + length:sizeof(psn)]; + NSAppleEventDescriptor* initial_event = + [NSAppleEventDescriptor + appleEventWithEventClass:app_mode::kAEChromeAppClass + eventID:app_mode::kAEChromeAppPing + targetDescriptor:target + returnID:kAutoGenerateReturnID + transactionID:kAnyTransactionID]; + + // Note that AESendMessage effectively ignores kAEDefaultTimeout, because this + // call does not pass kAEWantReceipt (which is deprecated and unsupported on + // Mac). Instead, rely on OnPingChromeTimeout(). + OSStatus status = AESendMessage( + [initial_event aeDesc], &replyEvent_, kAEQueueReply, kAEDefaultTimeout); + if (status != noErr) { + OSSTATUS_LOG(ERROR, status) << "AESendMessage"; + [self closeWithSuccess:false]; + } +} + +- (void)message:(NSAppleEventDescriptor*)event + withReply:(NSAppleEventDescriptor*)reply { + [self closeWithSuccess:true]; +} + +- (void)closeWithSuccess:(bool)success { + onReply_.Run(success); + NSAppleEventManager* em = [NSAppleEventManager sharedAppleEventManager]; + [em removeEventHandlerForEventClass:'aevt' andEventID:'ansr']; + [self release]; +} +@end + +//----------------------------------------------------------------------------- + +extern "C" { + +// |ChromeAppModeStart()| is the point of entry into the framework from the app +// mode loader. +__attribute__((visibility("default"))) +int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info); + +} // extern "C" + +int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { + CommandLine::Init(info->argc, info->argv); + + base::mac::ScopedNSAutoreleasePool scoped_pool; + base::AtExitManager exit_manager; + chrome::RegisterPathProvider(); + + if (info->major_version < app_mode::kCurrentChromeAppModeInfoMajorVersion) { + RAW_LOG(ERROR, "App Mode Loader too old."); + return 1; + } + if (info->major_version > app_mode::kCurrentChromeAppModeInfoMajorVersion) { + RAW_LOG(ERROR, "Browser Framework too old to load App Shortcut."); + return 1; + } + + g_info = info; + + // Set bundle paths. This loads the bundles. + base::mac::SetOverrideOuterBundlePath(g_info->chrome_outer_bundle_path); + base::mac::SetOverrideFrameworkBundlePath( + g_info->chrome_versioned_path.Append(chrome::kFrameworkName)); + + // Calculate the preferred locale used by Chrome. + // We can't use l10n_util::OverrideLocaleWithCocoaLocale() because it calls + // [base::mac::OuterBundle() preferredLocalizations] which gets localizations + // from the bundle of the running app (i.e. it is equivalent to + // [[NSBundle mainBundle] preferredLocalizations]) instead of the target + // bundle. + NSArray* preferred_languages = [NSLocale preferredLanguages]; + NSArray* supported_languages = [base::mac::OuterBundle() localizations]; + std::string preferred_localization; + for (NSString* language in preferred_languages) { + if ([supported_languages containsObject:language]) { + preferred_localization = base::SysNSStringToUTF8(language); + break; + } + } + std::string locale = l10n_util::NormalizeLocale( + l10n_util::GetApplicationLocale(preferred_localization)); + + // Load localized strings. + ui::ResourceBundle::InitSharedInstanceWithLocale( + locale, NULL, ui::ResourceBundle::DO_NOT_LOAD_COMMON_RESOURCES); + + // Launch the IO thread. + base::Thread::Options io_thread_options; + io_thread_options.message_loop_type = base::MessageLoop::TYPE_IO; + base::Thread *io_thread = new base::Thread("CrAppShimIO"); + io_thread->StartWithOptions(io_thread_options); + g_io_thread = io_thread; + + // Find already running instances of Chrome. + pid_t pid = -1; + std::string chrome_process_id = CommandLine::ForCurrentProcess()-> + GetSwitchValueASCII(app_mode::kLaunchedByChromeProcessId); + if (!chrome_process_id.empty()) { + if (!base::StringToInt(chrome_process_id, &pid)) + LOG(FATAL) << "Invalid PID: " << chrome_process_id; + } else { + NSString* chrome_bundle_id = [base::mac::OuterBundle() bundleIdentifier]; + NSArray* existing_chrome = [NSRunningApplication + runningApplicationsWithBundleIdentifier:chrome_bundle_id]; + if ([existing_chrome count] > 0) + pid = [[existing_chrome objectAtIndex:0] processIdentifier]; + } + + AppShimController controller; + base::MessageLoopForUI main_message_loop; + main_message_loop.set_thread_name("MainThread"); + base::PlatformThread::SetName("CrAppShimMain"); + + // In tests, launching Chrome does nothing, and we won't get a ping response, + // so just assume the socket exists. + if (pid == -1 && + !CommandLine::ForCurrentProcess()->HasSwitch( + app_mode::kLaunchedForTest)) { + // Launch Chrome if it isn't already running. + ProcessSerialNumber psn; + CommandLine command_line(CommandLine::NO_PROGRAM); + command_line.AppendSwitch(switches::kSilentLaunch); + + // If the shim is the app launcher, pass --show-app-list when starting a new + // Chrome process to inform startup codepaths and load the correct profile. + if (info->app_mode_id == app_mode::kAppListModeId) { + command_line.AppendSwitch(switches::kShowAppList); + } else { + command_line.AppendSwitchPath(switches::kProfileDirectory, + info->profile_dir); + } + + bool success = + base::mac::OpenApplicationWithPath(base::mac::OuterBundlePath(), + command_line, + kLSLaunchDefaults, + &psn); + if (!success) + return 1; + + base::Callback<void(bool)> on_ping_chrome_reply = + base::Bind(&AppShimController::OnPingChromeReply, + base::Unretained(&controller)); + + // This code abuses the fact that Apple Events sent before the process is + // fully initialized don't receive a reply until its run loop starts. Once + // the reply is received, Chrome will have opened its IPC port, guaranteed. + [ReplyEventHandler pingProcess:psn + andCall:on_ping_chrome_reply]; + + main_message_loop.PostDelayedTask( + FROM_HERE, + base::Bind(&AppShimController::OnPingChromeTimeout, + base::Unretained(&controller)), + base::TimeDelta::FromSeconds(kPingChromeTimeoutSeconds)); + } else { + // Chrome already running. Proceed to init. This could still fail if Chrome + // is still starting up or shutting down, but the process will exit quickly, + // which is preferable to waiting for the Apple Event to timeout after one + // minute. + main_message_loop.PostTask( + FROM_HERE, + base::Bind(&AppShimController::Init, + base::Unretained(&controller))); + } + + main_message_loop.Run(); + return 0; +} |