diff options
-rw-r--r-- | apps/DEPS | 4 | ||||
-rw-r--r-- | apps/app_shim/OWNERS | 1 | ||||
-rw-r--r-- | apps/app_shim/app_shim_host_mac.cc | 153 | ||||
-rw-r--r-- | apps/app_shim/app_shim_host_mac.h | 81 | ||||
-rw-r--r-- | apps/app_shim/app_shim_host_mac_unittest.cc | 124 | ||||
-rw-r--r-- | apps/app_shim/app_shim_host_manager_mac.h | 40 | ||||
-rw-r--r-- | apps/app_shim/app_shim_host_manager_mac.mm | 71 | ||||
-rw-r--r-- | apps/app_shim/app_shim_messages.h | 22 | ||||
-rw-r--r-- | apps/apps.gypi | 4 | ||||
-rw-r--r-- | chrome/app/DEPS | 1 | ||||
-rw-r--r-- | chrome/app/chrome_main_app_mode_mac.mm | 261 | ||||
-rw-r--r-- | chrome/browser/app_controller_mac.mm | 39 | ||||
-rw-r--r-- | chrome/browser/browser_process_impl.cc | 12 | ||||
-rw-r--r-- | chrome/browser/browser_process_impl.h | 9 | ||||
-rw-r--r-- | chrome/chrome_common.gypi | 1 | ||||
-rw-r--r-- | chrome/chrome_tests_unit.gypi | 1 | ||||
-rw-r--r-- | chrome/common/DEPS | 1 | ||||
-rw-r--r-- | chrome/common/common_message_generator.h | 1 | ||||
-rw-r--r-- | chrome/common/mac/app_mode_common.h | 14 | ||||
-rw-r--r-- | chrome/common/mac/app_mode_common.mm | 2 | ||||
-rw-r--r-- | ipc/ipc_message_start.h | 1 |
21 files changed, 772 insertions, 71 deletions
@@ -7,10 +7,14 @@ include_rules = [ "+chrome/browser/extensions", "+chrome/browser/profiles", "+chrome/browser/shell_integration.h", + "+chrome/browser/ui/extensions/application_launch.h", "+chrome/browser/ui/host_desktop.h", "+chrome/browser/ui/web_applications/web_app_ui.h", "+chrome/browser/web_applications/web_app.h", "+chrome/common/chrome_notification_types.h", + "+chrome/common/chrome_paths.h", "+chrome/common/extensions", + "+chrome/common/mac/app_mode_common.h", "+chrome/installer", + "+chrome/test/base/testing_profile.h", ] diff --git a/apps/app_shim/OWNERS b/apps/app_shim/OWNERS new file mode 100644 index 0000000..c046a32 --- /dev/null +++ b/apps/app_shim/OWNERS @@ -0,0 +1 @@ +jeremya@chromium.org diff --git a/apps/app_shim/app_shim_host_mac.cc b/apps/app_shim/app_shim_host_mac.cc new file mode 100644 index 0000000..8cff755 --- /dev/null +++ b/apps/app_shim/app_shim_host_mac.cc @@ -0,0 +1,153 @@ +// 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. + +#include "apps/app_shim/app_shim_host_mac.h" + +#include "apps/app_shim/app_shim_messages.h" +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/extensions/extension_host.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/shell_window_registry.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/extensions/application_launch.h" +#include "chrome/common/extensions/extension_constants.h" +#include "ipc/ipc_channel_proxy.h" + +AppShimHost::AppShimHost() + : channel_(NULL), profile_(NULL) { +} + +AppShimHost::~AppShimHost() { + DCHECK(CalledOnValidThread()); +} + +void AppShimHost::ServeChannel(const IPC::ChannelHandle& handle) { + DCHECK(CalledOnValidThread()); + DCHECK(!channel_.get()); + channel_.reset(new IPC::ChannelProxy(handle, IPC::Channel::MODE_SERVER, this, + BrowserThread::GetMessageLoopProxyForThread(BrowserThread::IO))); +} + +bool AppShimHost::OnMessageReceived(const IPC::Message& message) { + DCHECK(CalledOnValidThread()); + bool handled = true; + IPC_BEGIN_MESSAGE_MAP(AppShimHost, message) + IPC_MESSAGE_HANDLER(AppShimHostMsg_LaunchApp, OnLaunchApp) + IPC_MESSAGE_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + + return handled; +} + +bool AppShimHost::Send(IPC::Message* message) { + DCHECK(channel_.get()); + return channel_->Send(message); +} + +void AppShimHost::OnLaunchApp(std::string profile_dir, std::string app_id) { + DCHECK(CalledOnValidThread()); + bool success = LaunchAppImpl(profile_dir, app_id); + Send(new AppShimMsg_LaunchApp_Done(success)); +} + +bool AppShimHost::LaunchAppImpl(const std::string& profile_dir, + const std::string& app_id) { + DCHECK(CalledOnValidThread()); + if (profile_) { + // Only one app launch message per channel. + return false; + } + if (!extensions::Extension::IdIsValid(app_id)) { + LOG(ERROR) << "Bad app ID from app shim launch message."; + return false; + } + Profile* profile = FetchProfileForDirectory(profile_dir); + if (!profile) + return false; + + if (!LaunchApp(profile, app_id)) + return false; + + profile_ = profile; + app_id_ = app_id; + + registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED, + content::Source<Profile>(profile_)); + return true; +} + +Profile* AppShimHost::FetchProfileForDirectory(const std::string& profile_dir) { + ProfileManager* profileManager = g_browser_process->profile_manager(); + // Even though the name of this function is "unsafe", there's no security + // issue here -- the check for the profile name in the profile info cache + // ensures that we never access any directory that isn't a known profile. + base::FilePath path = base::FilePath::FromUTF8Unsafe(profile_dir); + path = profileManager->user_data_dir().Append(path); + ProfileInfoCache& cache = profileManager->GetProfileInfoCache(); + // This ensures that the given profile path is acutally a profile that we + // already know about. + if (cache.GetIndexOfProfileWithPath(path) == std::string::npos) { + LOG(ERROR) << "Requested directory is not a known profile."; + return NULL; + } + Profile* profile = profileManager->GetProfile(path); + if (!profile) { + LOG(ERROR) << "Couldn't get profile for directory '" << profile_dir << "'."; + return NULL; + } + return profile; +} + +bool AppShimHost::LaunchApp(Profile* profile, const std::string& app_id) { + extensions::ExtensionSystem* extension_system = + extensions::ExtensionSystem::Get(profile); + ExtensionServiceInterface* extension_service = + extension_system->extension_service(); + const extensions::Extension* extension = + extension_service->GetExtensionById( + app_id, false); + if (!extension) { + LOG(ERROR) << "Attempted to launch nonexistent app with id '" + << app_id << "'."; + return false; + } + // TODO(jeremya): Handle the case that launching the app fails. Probably we + // need to watch for 'app successfully launched' or at least 'background page + // exists/was created' and time out with failure if we don't see that sign of + // life within a certain window. + chrome::AppLaunchParams params(profile, + extension, + extension_misc::LAUNCH_NONE, + NEW_WINDOW); + chrome::OpenApplication(params); + return true; +} + +void AppShimHost::Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK(CalledOnValidThread()); + DCHECK(content::Source<Profile>(source).ptr() == profile_); + switch (type) { + case chrome::NOTIFICATION_EXTENSION_HOST_DESTROYED: { + extensions::ExtensionHost* extension_host = + content::Details<extensions::ExtensionHost>(details).ptr(); + if (app_id_ == extension_host->extension_id()) + Close(); + break; + } + default: + NOTREACHED() << "Unexpected notification sent."; + break; + } +} + +void AppShimHost::Close() { + DCHECK(CalledOnValidThread()); + delete this; +} diff --git a/apps/app_shim/app_shim_host_mac.h b/apps/app_shim/app_shim_host_mac.h new file mode 100644 index 0000000..8ebc0cd --- /dev/null +++ b/apps/app_shim/app_shim_host_mac.h @@ -0,0 +1,81 @@ +// 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. + +#ifndef CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MAC_H_ +#define CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MAC_H_ + +#include <string> + +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "ipc/ipc_listener.h" +#include "ipc/ipc_sender.h" + +class Profile; + +namespace IPC { +struct ChannelHandle; +class ChannelProxy; +class Message; +} // namespace IPC + +// This is the counterpart to AppShimController in +// chrome/app/chrome_main_app_mode_mac.mm. The AppShimHost owns itself, and is +// destroyed when the app it corresponds to is closed or when the channel +// connected to the app shim is closed. +class AppShimHost : public IPC::Listener, + public IPC::Sender, + public content::NotificationObserver, + public base::NonThreadSafe { + public: + AppShimHost(); + virtual ~AppShimHost(); + + // Creates a new server-side IPC channel at |handle|, which should contain a + // file descriptor of a channel created by an IPC::ChannelFactory, and begins + // listening for messages on it. + void ServeChannel(const IPC::ChannelHandle& handle); + + const std::string& app_id() const { return app_id_; } + const Profile* profile() const { return profile_; } + + protected: + // Used internally; virtual so they can be mocked for testing. + virtual Profile* FetchProfileForDirectory(const std::string& profile_dir); + virtual bool LaunchApp(Profile* profile, const std::string& app_id); + + // IPC::Listener implementation. + virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; + + // IPC::Sender implementation. + virtual bool Send(IPC::Message* message) OVERRIDE; + + private: + // The app shim process is requesting that an app be launched. Once it has + // done so the |profile| and |app_id| are stored, and all future messages + // from the app shim relate to the app it launched. It is an error for the + // app shim to send multiple launch messages. + void OnLaunchApp(std::string profile, std::string app_id); + + bool LaunchAppImpl(const std::string& profile_dir, const std::string& app_id); + + // The AppShimHost listens to the NOTIFICATION_EXTENSION_HOST_DESTROYED + // message to detect when the app closes. When that happens, the AppShimHost + // closes the channel, which causes the app shim process to quit. + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // Closes the channel and destroys the AppShimHost. + void Close(); + + scoped_ptr<IPC::ChannelProxy> channel_; + std::string app_id_; + Profile* profile_; + content::NotificationRegistrar registrar_; +}; + +#endif // CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MAC_H_ diff --git a/apps/app_shim/app_shim_host_mac_unittest.cc b/apps/app_shim/app_shim_host_mac_unittest.cc new file mode 100644 index 0000000..802c8e9 --- /dev/null +++ b/apps/app_shim/app_shim_host_mac_unittest.cc @@ -0,0 +1,124 @@ +// 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. + +#include "apps/app_shim/app_shim_host_mac.h" + +#include "apps/app_shim/app_shim_messages.h" +#include "base/memory/scoped_vector.h" +#include "chrome/test/base/testing_profile.h" +#include "ipc/ipc_message.h" +#include "testing/gtest/include/gtest/gtest.h" + +class TestingAppShimHost : public AppShimHost { + public: + explicit TestingAppShimHost(Profile* profile); + virtual ~TestingAppShimHost() {} + + bool ReceiveMessage(IPC::Message* message); + + const std::vector<IPC::Message*>& sent_messages() { + return sent_messages_.get(); + } + + void set_fails_profile(bool fails_profile) { + fails_profile_ = fails_profile; + } + + void set_fails_launch(bool fails_launch) { + fails_launch_ = fails_launch; + } + + protected: + virtual Profile* FetchProfileForDirectory(const std::string& profile_dir) + OVERRIDE; + virtual bool LaunchApp(Profile* profile, const std::string& app_id) OVERRIDE; + + virtual bool Send(IPC::Message* message) OVERRIDE; + + Profile* test_profile_; + bool fails_profile_; + bool fails_launch_; + + ScopedVector<IPC::Message> sent_messages_; + + DISALLOW_COPY_AND_ASSIGN(TestingAppShimHost); +}; + +TestingAppShimHost::TestingAppShimHost(Profile* profile) + : test_profile_(profile), + fails_profile_(false), + fails_launch_(false) { +} + +bool TestingAppShimHost::ReceiveMessage(IPC::Message* message) { + bool handled = OnMessageReceived(*message); + delete message; + return handled; +} + +bool TestingAppShimHost::Send(IPC::Message* message) { + sent_messages_.push_back(message); + return true; +} + +Profile* TestingAppShimHost::FetchProfileForDirectory( + const std::string& profile_dir) { + return fails_profile_ ? NULL : test_profile_; +} + +bool TestingAppShimHost::LaunchApp( + Profile* profile, const std::string& app_id) { + return !fails_launch_; +} + +class AppShimHostTest : public testing::Test { + public: + TestingAppShimHost* host() { return host_.get(); } + TestingProfile* profile() { return profile_.get(); } + + bool LaunchWasSuccessful() { + EXPECT_EQ(1u, host()->sent_messages().size()); + IPC::Message* message = host()->sent_messages()[0]; + EXPECT_EQ(AppShimMsg_LaunchApp_Done::ID, message->type()); + AppShimMsg_LaunchApp_Done::Param param; + AppShimMsg_LaunchApp_Done::Read(message, ¶m); + return param.a; + } + + private: + virtual void SetUp() OVERRIDE { + profile_.reset(new TestingProfile); + host_.reset(new TestingAppShimHost(profile())); + } + + scoped_ptr<TestingAppShimHost> host_; + scoped_ptr<TestingProfile> profile_; +}; + +static const std::string kTestAppId = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +static const std::string kTestProfileDir = "Default"; + +TEST_F(AppShimHostTest, TestLaunchApp) { + host()->ReceiveMessage( + new AppShimHostMsg_LaunchApp(kTestProfileDir, kTestAppId)); + ASSERT_EQ(kTestAppId, host()->app_id()); + ASSERT_EQ(profile(), host()->profile()); + ASSERT_TRUE(LaunchWasSuccessful()); +} + +TEST_F(AppShimHostTest, TestFailProfile) { + host()->set_fails_profile(true); + host()->ReceiveMessage( + new AppShimHostMsg_LaunchApp(kTestProfileDir, kTestAppId)); + ASSERT_TRUE(host()->app_id().empty()); + ASSERT_FALSE(LaunchWasSuccessful()); +} + +TEST_F(AppShimHostTest, TestFailLaunch) { + host()->set_fails_launch(true); + host()->ReceiveMessage( + new AppShimHostMsg_LaunchApp(kTestProfileDir, kTestAppId)); + ASSERT_TRUE(host()->app_id().empty()); + ASSERT_FALSE(LaunchWasSuccessful()); +} diff --git a/apps/app_shim/app_shim_host_manager_mac.h b/apps/app_shim/app_shim_host_manager_mac.h new file mode 100644 index 0000000..13283ee --- /dev/null +++ b/apps/app_shim/app_shim_host_manager_mac.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MANAGER_MAC_H_ +#define CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MANAGER_MAC_H_ + +#include "base/mac/scoped_cftyperef.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread.h" +#include "ipc/ipc_channel_factory.h" + +// The AppShimHostManager receives connections from app shims on a UNIX +// socket (|factory_|) and creates a helper object to manage the connection. +class AppShimHostManager + : public IPC::ChannelFactory::Delegate, + public base::SupportsWeakPtr<AppShimHostManager> { + public: + AppShimHostManager(); + virtual ~AppShimHostManager(); + + private: + // IPC::ChannelFactory::Delegate implementation. + virtual void OnClientConnected(const IPC::ChannelHandle& handle) OVERRIDE; + virtual void OnListenError() OVERRIDE; + + // The |factory_| must be created on a thread which allows blocking I/O, so + // part of the initialization of this class must be carried out on the file + // thread. + void InitOnFileThread(); + + // Called on the IO thread to begin listening for connections from app shims. + void ListenOnIOThread(); + + scoped_ptr<IPC::ChannelFactory> factory_; + + DISALLOW_COPY_AND_ASSIGN(AppShimHostManager); +}; + +#endif // CHROME_BROWSER_WEB_APPLICATIONS_APP_SHIM_HOST_MANAGER_MAC_H_ diff --git a/apps/app_shim/app_shim_host_manager_mac.mm b/apps/app_shim/app_shim_host_manager_mac.mm new file mode 100644 index 0000000..6133b9b --- /dev/null +++ b/apps/app_shim/app_shim_host_manager_mac.mm @@ -0,0 +1,71 @@ +// 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. + +#include "apps/app_shim/app_shim_host_manager_mac.h" + +#include "apps/app_shim/app_shim_host_mac.h" +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/logging.h" +#include "base/path_service.h" +#include "chrome/browser/browser_process.h" +#include "chrome/common/chrome_paths.h" +#include "content/public/browser/browser_thread.h" +#include "chrome/common/mac/app_mode_common.h" + +using content::BrowserThread; + +namespace { + +void CreateAppShimHost(const IPC::ChannelHandle& handle) { + // AppShimHost takes ownership of itself. + (new AppShimHost)->ServeChannel(handle); +} + +} // namespace + +AppShimHostManager::AppShimHostManager() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + BrowserThread::PostTask( + BrowserThread::FILE, FROM_HERE, + base::Bind(&AppShimHostManager::InitOnFileThread, + base::Unretained(this))); +} + +AppShimHostManager::~AppShimHostManager() { +} + +void AppShimHostManager::InitOnFileThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + base::FilePath user_data_dir; + if (!PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) { + LOG(ERROR) << "Couldn't get user data directory while creating App Shim " + << "Host manager."; + return; + } + base::FilePath socket_path = + user_data_dir.Append(app_mode::kAppShimSocketName); + factory_.reset(new IPC::ChannelFactory(socket_path, this)); + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + base::Bind(&AppShimHostManager::ListenOnIOThread, + base::Unretained(this))); +} + +void AppShimHostManager::ListenOnIOThread() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + factory_->Listen(); +} + +void AppShimHostManager::OnClientConnected( + const IPC::ChannelHandle& handle) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + base::Bind(&CreateAppShimHost, handle)); +} + +void AppShimHostManager::OnListenError() { + // TODO(jeremya): set a timeout and attempt to reconstruct the channel. +} diff --git a/apps/app_shim/app_shim_messages.h b/apps/app_shim/app_shim_messages.h new file mode 100644 index 0000000..1e49ce4 --- /dev/null +++ b/apps/app_shim/app_shim_messages.h @@ -0,0 +1,22 @@ +// 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. + +// Multiply-included message file, hence no include guard. + +#include <string> + +#include "ipc/ipc_message_macros.h" + +#define IPC_MESSAGE_START AppShimMsgStart + +// Signals that a previous LaunchApp message has been processed, and lets the +// app shim process know whether the app launch was successful. +IPC_MESSAGE_CONTROL1(AppShimMsg_LaunchApp_Done, + bool /* succeeded */) + +// Tells the main Chrome process to launch a particular app with the given +// profile name and app id. +IPC_MESSAGE_CONTROL2(AppShimHostMsg_LaunchApp, + std::string /* profile name */, + std::string /* app id */) diff --git a/apps/apps.gypi b/apps/apps.gypi index e817475..aa2330b 100644 --- a/apps/apps.gypi +++ b/apps/apps.gypi @@ -22,6 +22,10 @@ '<(INTERMEDIATE_DIR)', ], 'sources': [ + 'app_shim/app_shim_host_mac.cc', + 'app_shim/app_shim_host_mac.h', + 'app_shim/app_shim_host_manager_mac.h', + 'app_shim/app_shim_host_manager_mac.mm', 'app_launcher.cc', 'app_launcher.h', 'app_restore_service.cc', diff --git a/chrome/app/DEPS b/chrome/app/DEPS index 4ae5783..27f8bf7 100644 --- a/chrome/app/DEPS +++ b/chrome/app/DEPS @@ -1,4 +1,5 @@ include_rules = [ + "+apps", "+breakpad", "+chrome/browser", "+chrome/installer", diff --git a/chrome/app/chrome_main_app_mode_mac.mm b/chrome/app/chrome_main_app_mode_mac.mm index d102fc8..a75f38d 100644 --- a/chrome/app/chrome_main_app_mode_mac.mm +++ b/chrome/app/chrome_main_app_mode_mac.mm @@ -7,20 +7,219 @@ // passing the appropriate data. This is the entry point into the framework for // those app bundles. -#include "base/basictypes.h" -#include "base/files/file_path.h" +#import <Cocoa/Cocoa.h> + +#include "apps/app_shim/app_shim_messages.h" +#include "base/at_exit.h" #include "base/logging.h" -#include "base/mac/bundle_locations.h" -#include "base/mac/foundation_util.h" #include "base/mac/mac_logging.h" #include "base/mac/mac_util.h" #include "base/mac/scoped_nsautorelease_pool.h" +#include "base/message_loop.h" +#include "base/path_service.h" #include "base/sys_string_conversions.h" -#include "chrome/browser/shell_integration.h" -#include "chrome/common/chrome_constants.h" -#include "chrome/common/chrome_paths_internal.h" +#include "base/threading/thread.h" +#include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/mac/app_mode_common.h" +#include "ipc/ipc_channel_proxy.h" +#include "ipc/ipc_listener.h" +#include "ipc/ipc_message.h" + +namespace { + +const app_mode::ChromeAppModeInfo* g_info; +base::Thread* g_io_thread = NULL; + +} // namespace + +// 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(); + + // Connects to Chrome and sends a LaunchApp message. + void Init(); + + 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(bool success); + + // Quits the app shim process. + void Quit(); + + IPC::ChannelProxy* channel_; + + DISALLOW_COPY_AND_ASSIGN(AppShimController); +}; + +AppShimController::AppShimController() : channel_(NULL) { +} + +void AppShimController::Init() { + DCHECK(g_io_thread); + base::FilePath user_data_dir; + if (!PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) { + Quit(); + return; + } + base::FilePath socket_path = + user_data_dir.Append(app_mode::kAppShimSocketName); + IPC::ChannelHandle handle(socket_path.value()); + channel_ = new IPC::ChannelProxy(handle, IPC::Channel::MODE_NAMED_CLIENT, + this, g_io_thread->message_loop_proxy()); + + channel_->Send(new AppShimHostMsg_LaunchApp( + g_info->profile_dir.value(), g_info->app_mode_id)); +} + +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_UNHANDLED(handled = false) + IPC_END_MESSAGE_MAP() + + return handled; +} + +void AppShimController::OnChannelError() { + LOG(ERROR) << "App shim channel error."; + Quit(); +} + +void AppShimController::OnLaunchAppDone(bool success) { + if (!success) + Quit(); +} + +void AppShimController::Quit() { + [NSApp terminate:nil]; +} + +//----------------------------------------------------------------------------- + +// 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]; + // And away we go. + // TODO(jeremya): if we don't care about the contents of the reply, can we + // pass NULL for the reply event parameter? + 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 + +//----------------------------------------------------------------------------- + +namespace { + +// 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) { + if (!success) { + [NSApp terminate:nil]; + return; + } + AppShimController* controller = new AppShimController; + controller->Init(); +} + +} // namespace extern "C" { @@ -33,6 +232,8 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info); int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { 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."); @@ -43,11 +244,21 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { return 1; } + g_info = info; + + // Launch the IO thread. + base::Thread::Options io_thread_options; + io_thread_options.message_loop_type = MessageLoop::TYPE_IO; + base::Thread *io_thread = new base::Thread("CrAppShimIO"); + io_thread->StartWithOptions(io_thread_options); + g_io_thread = io_thread; + + // Launch Chrome if it isn't already running. FSRef app_fsref; if (!base::mac::FSRefFromPath(info->chrome_outer_bundle_path.value(), &app_fsref)) { - PLOG(ERROR) << "base::mac::FSRefFromPath failed for " - << info->chrome_outer_bundle_path.value(); + LOG(ERROR) << "base::mac::FSRefFromPath failed for " + << info->chrome_outer_bundle_path.value(); return 1; } std::string silent = std::string("--") + switches::kSilentLaunch; @@ -63,27 +274,23 @@ int ChromeAppModeStart(const app_mode::ChromeAppModeInfo* info) { launch_args, NULL // initialEvent }; - NSAppleEventDescriptor* initial_event = - [NSAppleEventDescriptor - appleEventWithEventClass:app_mode::kAEChromeAppClass - eventID:app_mode::kAEChromeAppLaunch - targetDescriptor:nil - returnID:kAutoGenerateReturnID - transactionID:kAnyTransactionID]; - NSAppleEventDescriptor* appid_descriptor = [NSAppleEventDescriptor - descriptorWithString:base::SysUTF8ToNSString(info->app_mode_id)]; - [initial_event setParamDescriptor:appid_descriptor - forKeyword:keyDirectObject]; - NSAppleEventDescriptor* profile_dir_descriptor = [NSAppleEventDescriptor - descriptorWithString:base::SysUTF8ToNSString(info->profile_dir.value())]; - [initial_event setParamDescriptor:profile_dir_descriptor - forKeyword:app_mode::kAEProfileDirKey]; - ls_parameters.initialEvent = const_cast<AEDesc*>([initial_event aeDesc]); - // Send the Apple Event using launch services, launching Chrome if necessary. - OSStatus status = LSOpenApplication(&ls_parameters, NULL); + ProcessSerialNumber psn; + // TODO(jeremya): this opens a new browser window if Chrome is already + // running without any windows open. + OSStatus status = LSOpenApplication(&ls_parameters, &psn); if (status != noErr) { OSSTATUS_LOG(ERROR, status) << "LSOpenApplication"; return 1; } + + // 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:base::Bind(&OnPingChromeReply)]; + + MessageLoopForUI main_message_loop; + main_message_loop.set_thread_name("MainThread"); + base::PlatformThread::SetName("CrAppShimMain"); + main_message_loop.Run(); return 0; } diff --git a/chrome/browser/app_controller_mac.mm b/chrome/browser/app_controller_mac.mm index 14135fb..89a0240 100644 --- a/chrome/browser/app_controller_mac.mm +++ b/chrome/browser/app_controller_mac.mm @@ -184,8 +184,6 @@ void RecordLastRunAppBundlePath() { - (void)getUrl:(NSAppleEventDescriptor*)event withReply:(NSAppleEventDescriptor*)reply; - (void)submitCloudPrintJob:(NSAppleEventDescriptor*)event; -- (void)launchPlatformApp:(NSAppleEventDescriptor*)event - withReply:(NSAppleEventDescriptor*)reply; - (void)windowLayeringDidChange:(NSNotification*)inNotification; - (void)windowChangedToProfile:(Profile*)profile; - (void)checkForAnyKeyWindows; @@ -217,11 +215,6 @@ void RecordLastRunAppBundlePath() { forEventClass:'WWW!' // A particularly ancient AppleEvent that dates andEventID:'OURL']; // back to the Spyglass days. - [em setEventHandler:self - andSelector:@selector(launchPlatformApp:withReply:) - forEventClass:app_mode::kAEChromeAppClass - andEventID:app_mode::kAEChromeAppLaunch]; - // Register for various window layering changes. We use these to update // various UI elements (command-key equivalents, etc) when the frontmost // window changes. @@ -1146,38 +1139,6 @@ void RecordLastRunAppBundlePath() { [self openUrls:gurlVector]; } -- (void)launchPlatformApp:(NSAppleEventDescriptor*)event - withReply:(NSAppleEventDescriptor*)reply { - NSString* appId = - [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; - NSString* profileDir = - [[event paramDescriptorForKeyword:app_mode::kAEProfileDirKey] - stringValue]; - - ProfileManager* profileManager = g_browser_process->profile_manager(); - base::FilePath path = base::FilePath(base::SysNSStringToUTF8(profileDir)); - path = profileManager->user_data_dir().Append(path); - Profile* profile = profileManager->GetProfile(path); - if (!profile) { - LOG(ERROR) << "Unable to locate a suitable profile for profile directory '" - << profileDir << "' while trying to load app with id '" - << appId << "'."; - return; - } - ExtensionServiceInterface* extensionService = - extensions::ExtensionSystem::Get(profile)->extension_service(); - const extensions::Extension* extension = - extensionService->GetExtensionById( - base::SysNSStringToUTF8(appId), false); - if (!extension) { - LOG(ERROR) << "Shortcut attempted to launch nonexistent app with id '" - << base::SysNSStringToUTF8(appId) << "'."; - return; - } - chrome::OpenApplication(chrome::AppLaunchParams( - profile, extension, extension_misc::LAUNCH_NONE, NEW_WINDOW)); -} - // Apple Event handler that receives print event from service // process, gets the required data and launches Print dialog. - (void)submitCloudPrintJob:(NSAppleEventDescriptor*)event { diff --git a/chrome/browser/browser_process_impl.cc b/chrome/browser/browser_process_impl.cc index 6dfde51..574a2f8 100644 --- a/chrome/browser/browser_process_impl.cc +++ b/chrome/browser/browser_process_impl.cc @@ -120,6 +120,10 @@ #include "chrome/browser/plugins/plugins_resource_service.h" #endif +#if defined(OS_MACOSX) +#include "apps/app_shim/app_shim_host_manager_mac.h" +#endif + #if (defined(OS_WIN) || defined(OS_LINUX)) && !defined(OS_CHROMEOS) // How often to check if the persistent instance of Chrome needs to restart // to install an update. @@ -266,6 +270,10 @@ void BrowserProcessImpl::StartTearDown() { // monitor information. aura::Env::DeleteInstance(); #endif + +#if defined(OS_MACOSX) + app_shim_host_manager_.reset(); +#endif } void BrowserProcessImpl::PostDestroyThreads() { @@ -896,6 +904,10 @@ void BrowserProcessImpl::PreMainMessageLoopRun() { bookmark_prompt_controller_.reset(new BookmarkPromptController()); } #endif + +#if defined(OS_MACOSX) + app_shim_host_manager_.reset(new AppShimHostManager); +#endif } void BrowserProcessImpl::CreateIconManager() { diff --git a/chrome/browser/browser_process_impl.h b/chrome/browser/browser_process_impl.h index 076ea73..bd43d99 100644 --- a/chrome/browser/browser_process_impl.h +++ b/chrome/browser/browser_process_impl.h @@ -45,6 +45,10 @@ class PolicyService; class MetroViewerProcessHost; #endif +#if defined(OS_MACOSX) +class AppShimHostManager; +#endif + // Real implementation of BrowserProcess that creates and returns the services. class BrowserProcessImpl : public BrowserProcess, public base::NonThreadSafe { @@ -299,6 +303,11 @@ class BrowserProcessImpl : public BrowserProcess, scoped_ptr<MetroViewerProcessHost> metro_viewer_process_host_; #endif +#if defined(OS_MACOSX) + // Hosts the IPC channel factory that App Shims connect to on Mac. + scoped_ptr<AppShimHostManager> app_shim_host_manager_; +#endif + // TODO(eroman): Remove this when done debugging 113031. This tracks // the callstack which released the final module reference count. base::debug::StackTrace release_last_reference_callstack_; diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi index 1ed50df..f1103ef 100644 --- a/chrome/chrome_common.gypi +++ b/chrome/chrome_common.gypi @@ -52,6 +52,7 @@ '<(DEPTH)/webkit/support/webkit_support.gyp:user_agent', ], 'sources': [ + '../apps/app_shim/app_shim_messages.h', '../extensions/common/constants.cc', '../extensions/common/constants.h', '../extensions/common/draggable_region.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index baa61afa..85c415d 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -454,6 +454,7 @@ }, }, 'sources': [ + '../apps/app_shim/app_shim_host_mac_unittest.cc', '../extensions/common/event_filter_unittest.cc', '../extensions/common/matcher/regex_set_matcher_unittest.cc', '../extensions/common/matcher/string_pattern_unittest.cc', diff --git a/chrome/common/DEPS b/chrome/common/DEPS index 2c56a2a..928db71 100644 --- a/chrome/common/DEPS +++ b/chrome/common/DEPS @@ -1,4 +1,5 @@ include_rules = [ + "+apps/app_shim", # For app shim messages. "+breakpad", # For Breakpad constants. "+chrome/plugin", # For checking whether we're a plugin process. "+components/autofill/common", diff --git a/chrome/common/common_message_generator.h b/chrome/common/common_message_generator.h index 39d3b58e..919324e 100644 --- a/chrome/common/common_message_generator.h +++ b/chrome/common/common_message_generator.h @@ -4,6 +4,7 @@ // Multiply-included file, hence no include guard. +#include "apps/app_shim/app_shim_messages.h" #include "chrome/common/benchmarking_messages.h" #include "chrome/common/chrome_utility_messages.h" #include "chrome/common/extensions/extension_messages.h" diff --git a/chrome/common/mac/app_mode_common.h b/chrome/common/mac/app_mode_common.h index e4ccb04..7a442da 100644 --- a/chrome/common/mac/app_mode_common.h +++ b/chrome/common/mac/app_mode_common.h @@ -15,12 +15,16 @@ namespace app_mode { -// Keys for a custom 'launch platform app' Apple event. When Chrome receives -// this event, it should launch the app specified by the direct key in the -// event, in the profile specified by the 'pdir' key. +// These are keys for an Apple Event ping that the app shim process sends to +// Chrome to get confirmation that Chrome is alive. The main Chrome process +// doesn't need to register any handlers for them -- the event is just sent for +// the empty reply that's automatically returned by the system. const AEEventClass kAEChromeAppClass = 'cApp'; -const AEEventID kAEChromeAppLaunch = 'lnch'; -const AEKeyword kAEProfileDirKey = 'pdir'; +const AEEventID kAEChromeAppPing = 'ping'; + +// The IPC socket used to communicate between app shims and Chrome will be +// created under the user data directory with this name. +extern const char kAppShimSocketName[]; // The key under which the browser's bundle ID will be stored in the // app mode launcher bundle's Info.plist. diff --git a/chrome/common/mac/app_mode_common.mm b/chrome/common/mac/app_mode_common.mm index 3efcccb..41330bf 100644 --- a/chrome/common/mac/app_mode_common.mm +++ b/chrome/common/mac/app_mode_common.mm @@ -6,6 +6,8 @@ namespace app_mode { +const char kAppShimSocketName[] = "App Shim Socket"; + NSString* const kBrowserBundleIDKey = @"CrBundleIdentifier"; NSString* const kCrAppModeShortcutIDKey = @"CrAppModeShortcutID"; NSString* const kCrAppModeShortcutNameKey = @"CrAppModeShortcutName"; diff --git a/ipc/ipc_message_start.h b/ipc/ipc_message_start.h index a277f02..b535e92 100644 --- a/ipc/ipc_message_start.h +++ b/ipc/ipc_message_start.h @@ -75,6 +75,7 @@ enum IPCMessageStart { PeerConnectionTrackerMsgStart, VisitedLinkMsgStart, OneClickSigninMsgStart, + AppShimMsgStart, LastIPCMsgStart // Must come last. }; |