Home | History | Annotate | Download | only in app_shim
      1 // Copyright 2014 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.
      4 
      5 #import <Cocoa/Cocoa.h>
      6 #include <vector>
      7 
      8 #include "apps/switches.h"
      9 #include "base/auto_reset.h"
     10 #include "base/callback.h"
     11 #include "base/files/file_path_watcher.h"
     12 #include "base/mac/foundation_util.h"
     13 #include "base/mac/launch_services_util.h"
     14 #include "base/mac/mac_util.h"
     15 #include "base/mac/scoped_nsobject.h"
     16 #include "base/path_service.h"
     17 #include "base/process/launch.h"
     18 #include "base/strings/sys_string_conversions.h"
     19 #include "base/test/test_timeouts.h"
     20 #include "chrome/browser/apps/app_browsertest_util.h"
     21 #include "chrome/browser/apps/app_shim/app_shim_handler_mac.h"
     22 #include "chrome/browser/apps/app_shim/app_shim_host_manager_mac.h"
     23 #include "chrome/browser/apps/app_shim/extension_app_shim_handler_mac.h"
     24 #include "chrome/browser/browser_process.h"
     25 #include "chrome/browser/profiles/profile.h"
     26 #include "chrome/browser/web_applications/web_app_mac.h"
     27 #include "chrome/common/chrome_paths.h"
     28 #include "chrome/common/chrome_switches.h"
     29 #include "chrome/common/mac/app_mode_common.h"
     30 #include "content/public/test/test_utils.h"
     31 #include "extensions/browser/app_window/native_app_window.h"
     32 #include "extensions/browser/extension_registry.h"
     33 #include "extensions/test/extension_test_message_listener.h"
     34 #import "ui/events/test/cocoa_test_event_utils.h"
     35 
     36 namespace {
     37 
     38 // General end-to-end test for app shims.
     39 class AppShimInteractiveTest : public extensions::PlatformAppBrowserTest {
     40  protected:
     41   AppShimInteractiveTest()
     42       : auto_reset_(&g_app_shims_allow_update_and_launch_in_tests, true) {}
     43 
     44  private:
     45   // Temporarily enable app shims.
     46   base::AutoReset<bool> auto_reset_;
     47 
     48   DISALLOW_COPY_AND_ASSIGN(AppShimInteractiveTest);
     49 };
     50 
     51 // Watches for changes to a file. This is designed to be used from the the UI
     52 // thread.
     53 class WindowedFilePathWatcher
     54     : public base::RefCountedThreadSafe<WindowedFilePathWatcher> {
     55  public:
     56   WindowedFilePathWatcher(const base::FilePath& path)
     57       : path_(path), observed_(false) {
     58     content::BrowserThread::PostTask(
     59         content::BrowserThread::FILE,
     60         FROM_HERE,
     61         base::Bind(&WindowedFilePathWatcher::Watch, this));
     62   }
     63 
     64   void Wait() {
     65     if (observed_)
     66       return;
     67 
     68     run_loop_.reset(new base::RunLoop);
     69     run_loop_->Run();
     70   }
     71 
     72  protected:
     73   friend class base::RefCountedThreadSafe<WindowedFilePathWatcher>;
     74   virtual ~WindowedFilePathWatcher() {}
     75 
     76   void Watch() {
     77     watcher_.reset(new base::FilePathWatcher);
     78     watcher_->Watch(path_.DirName(),
     79                     false,
     80                     base::Bind(&WindowedFilePathWatcher::Observe, this));
     81   }
     82 
     83   void Observe(const base::FilePath& path, bool error) {
     84     DCHECK(!error);
     85     if (base::PathExists(path_)) {
     86       watcher_.reset();
     87       content::BrowserThread::PostTask(
     88           content::BrowserThread::UI,
     89           FROM_HERE,
     90           base::Bind(&WindowedFilePathWatcher::StopRunLoop, this));
     91     }
     92   }
     93 
     94   void StopRunLoop() {
     95     observed_ = true;
     96     if (run_loop_.get())
     97       run_loop_->Quit();
     98   }
     99 
    100  private:
    101   const base::FilePath path_;
    102   scoped_ptr<base::FilePathWatcher> watcher_;
    103   bool observed_;
    104   scoped_ptr<base::RunLoop> run_loop_;
    105 
    106   DISALLOW_COPY_AND_ASSIGN(WindowedFilePathWatcher);
    107 };
    108 
    109 // Watches for an app shim to connect.
    110 class WindowedAppShimLaunchObserver : public apps::AppShimHandler {
    111  public:
    112   WindowedAppShimLaunchObserver(const std::string& app_id)
    113       : app_mode_id_(app_id),
    114         observed_(false) {
    115     apps::AppShimHandler::RegisterHandler(app_id, this);
    116   }
    117 
    118   void Wait() {
    119     if (observed_)
    120       return;
    121 
    122     run_loop_.reset(new base::RunLoop);
    123     run_loop_->Run();
    124   }
    125 
    126   // AppShimHandler overrides:
    127   virtual void OnShimLaunch(Host* host,
    128                             apps::AppShimLaunchType launch_type,
    129                             const std::vector<base::FilePath>& files) OVERRIDE {
    130     // Remove self and pass through to the default handler.
    131     apps::AppShimHandler::RemoveHandler(app_mode_id_);
    132     apps::AppShimHandler::GetForAppMode(app_mode_id_)
    133         ->OnShimLaunch(host, launch_type, files);
    134     observed_ = true;
    135     if (run_loop_.get())
    136       run_loop_->Quit();
    137   }
    138   virtual void OnShimClose(Host* host) OVERRIDE {}
    139   virtual void OnShimFocus(Host* host,
    140                            apps::AppShimFocusType focus_type,
    141                            const std::vector<base::FilePath>& files) OVERRIDE {}
    142   virtual void OnShimSetHidden(Host* host, bool hidden) OVERRIDE {}
    143   virtual void OnShimQuit(Host* host) OVERRIDE {}
    144 
    145  private:
    146   std::string app_mode_id_;
    147   bool observed_;
    148   scoped_ptr<base::RunLoop> run_loop_;
    149 
    150   DISALLOW_COPY_AND_ASSIGN(WindowedAppShimLaunchObserver);
    151 };
    152 
    153 NSString* GetBundleID(const base::FilePath& shim_path) {
    154   base::FilePath plist_path = shim_path.Append("Contents").Append("Info.plist");
    155   NSMutableDictionary* plist = [NSMutableDictionary
    156       dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
    157   return [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
    158 }
    159 
    160 bool HasAppShimHost(Profile* profile, const std::string& app_id) {
    161   return g_browser_process->platform_part()
    162       ->app_shim_host_manager()
    163       ->extension_app_shim_handler()
    164       ->FindHost(profile, app_id);
    165 }
    166 
    167 }  // namespace
    168 
    169 // Watches for NSNotifications from the shared workspace.
    170 @interface WindowedNSNotificationObserver : NSObject {
    171  @private
    172   base::scoped_nsobject<NSString> bundleId_;
    173   BOOL notificationReceived_;
    174   scoped_ptr<base::RunLoop> runLoop_;
    175 }
    176 
    177 - (id)initForNotification:(NSString*)name
    178               andBundleId:(NSString*)bundleId;
    179 - (void)observe:(NSNotification*)notification;
    180 - (void)wait;
    181 @end
    182 
    183 @implementation WindowedNSNotificationObserver
    184 
    185 - (id)initForNotification:(NSString*)name
    186               andBundleId:(NSString*)bundleId {
    187   if (self = [super init]) {
    188     bundleId_.reset([[bundleId copy] retain]);
    189     [[[NSWorkspace sharedWorkspace] notificationCenter]
    190         addObserver:self
    191            selector:@selector(observe:)
    192                name:name
    193              object:nil];
    194   }
    195   return self;
    196 }
    197 
    198 - (void)observe:(NSNotification*)notification {
    199   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    200 
    201   NSRunningApplication* application =
    202       [[notification userInfo] objectForKey:NSWorkspaceApplicationKey];
    203   if (![[application bundleIdentifier] isEqualToString:bundleId_])
    204     return;
    205 
    206   [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
    207   notificationReceived_ = YES;
    208   if (runLoop_.get())
    209     runLoop_->Quit();
    210 }
    211 
    212 - (void)wait {
    213   if (notificationReceived_)
    214     return;
    215 
    216   runLoop_.reset(new base::RunLoop);
    217   runLoop_->Run();
    218 }
    219 
    220 @end
    221 
    222 namespace apps {
    223 
    224 // Shims require static libraries http://crbug.com/386024.
    225 #if defined(COMPONENT_BUILD)
    226 #define MAYBE_Launch DISABLED_Launch
    227 #define MAYBE_RebuildShim DISABLED_RebuildShim
    228 #else
    229 #define MAYBE_Launch Launch
    230 #define MAYBE_RebuildShim RebuildShim
    231 #endif
    232 
    233 // Test that launching the shim for an app starts the app, and vice versa.
    234 // These two cases are combined because the time to run the test is dominated
    235 // by loading the extension and creating the shim.
    236 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_Launch) {
    237   // Install the app.
    238   const extensions::Extension* app = InstallPlatformApp("minimal");
    239 
    240   // Use a WebAppShortcutCreator to get the path.
    241   web_app::WebAppShortcutCreator shortcut_creator(
    242       web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
    243       web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
    244       extensions::FileHandlersInfo());
    245   base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
    246   EXPECT_FALSE(base::PathExists(shim_path));
    247 
    248   // Create the internal app shim by simulating an app update. FilePathWatcher
    249   // is used to wait for file operations on the shim to be finished before
    250   // attempting to launch it. Since all of the file operations are done in the
    251   // same event on the FILE thread, everything will be done by the time the
    252   // watcher's callback is executed.
    253   scoped_refptr<WindowedFilePathWatcher> file_watcher =
    254       new WindowedFilePathWatcher(shim_path);
    255   web_app::UpdateAllShortcuts(base::string16(), profile(), app);
    256   file_watcher->Wait();
    257   ASSERT_TRUE(base::PathExists(shim_path));
    258   NSString* bundle_id = GetBundleID(shim_path);
    259 
    260   // Case 1: Launch the app, it should start the shim.
    261   {
    262     base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
    263     ns_observer.reset([[WindowedNSNotificationObserver alloc]
    264         initForNotification:NSWorkspaceDidLaunchApplicationNotification
    265                 andBundleId:bundle_id]);
    266     WindowedAppShimLaunchObserver observer(app->id());
    267     LaunchPlatformApp(app);
    268     [ns_observer wait];
    269     observer.Wait();
    270 
    271     EXPECT_TRUE(GetFirstAppWindow());
    272     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
    273 
    274     // Quitting the shim will eventually cause it to quit. It actually
    275     // intercepts the -terminate, sends an AppShimHostMsg_QuitApp to Chrome,
    276     // and returns NSTerminateLater. Chrome responds by closing all windows of
    277     // the app. Once all windows are closed, Chrome closes the IPC channel,
    278     // which causes the shim to actually terminate.
    279     NSArray* running_shim = [NSRunningApplication
    280         runningApplicationsWithBundleIdentifier:bundle_id];
    281     ASSERT_EQ(1u, [running_shim count]);
    282 
    283     ns_observer.reset([[WindowedNSNotificationObserver alloc]
    284         initForNotification:NSWorkspaceDidTerminateApplicationNotification
    285                 andBundleId:bundle_id]);
    286     [base::mac::ObjCCastStrict<NSRunningApplication>(
    287         [running_shim objectAtIndex:0]) terminate];
    288     [ns_observer wait];
    289 
    290     EXPECT_FALSE(GetFirstAppWindow());
    291     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
    292   }
    293 
    294   // Case 2: Launch the shim, it should start the app.
    295   {
    296     ExtensionTestMessageListener launched_listener("Launched", false);
    297     CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
    298     shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
    299     ProcessSerialNumber shim_psn;
    300     ASSERT_TRUE(base::mac::OpenApplicationWithPath(
    301         shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
    302     ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
    303 
    304     ASSERT_TRUE(GetFirstAppWindow());
    305     EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
    306 
    307     // If the window is closed, the shim should quit.
    308     pid_t shim_pid;
    309     EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
    310     GetFirstAppWindow()->GetBaseWindow()->Close();
    311     ASSERT_TRUE(
    312         base::WaitForSingleProcess(shim_pid, TestTimeouts::action_timeout()));
    313 
    314     EXPECT_FALSE(GetFirstAppWindow());
    315     EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
    316   }
    317 }
    318 
    319 #if defined(ARCH_CPU_64_BITS)
    320 
    321 // Tests that a 32 bit shim attempting to launch 64 bit Chrome will eventually
    322 // be rebuilt.
    323 IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_RebuildShim) {
    324   // Get the 32 bit shim.
    325   base::FilePath test_data_dir;
    326   PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
    327   base::FilePath shim_path_32 =
    328       test_data_dir.Append("app_shim").Append("app_shim_32_bit.app");
    329   EXPECT_TRUE(base::PathExists(shim_path_32));
    330 
    331   // Install test app.
    332   const extensions::Extension* app = InstallPlatformApp("minimal");
    333 
    334   // Use WebAppShortcutCreator to create a 64 bit shim.
    335   web_app::WebAppShortcutCreator shortcut_creator(
    336       web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
    337       web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
    338       extensions::FileHandlersInfo());
    339   shortcut_creator.UpdateShortcuts();
    340   base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
    341   NSMutableDictionary* plist_64 = [NSMutableDictionary
    342       dictionaryWithContentsOfFile:base::mac::FilePathToNSString(
    343           shim_path.Append("Contents").Append("Info.plist"))];
    344 
    345   // Copy 32 bit shim to where it's expected to be.
    346   // CopyDirectory doesn't seem to work when copying and renaming in one go.
    347   ASSERT_TRUE(base::DeleteFile(shim_path, true));
    348   ASSERT_TRUE(base::PathExists(shim_path.DirName()));
    349   ASSERT_TRUE(base::CopyDirectory(shim_path_32, shim_path.DirName(), true));
    350   ASSERT_TRUE(base::Move(shim_path.DirName().Append(shim_path_32.BaseName()),
    351                          shim_path));
    352   ASSERT_TRUE(base::PathExists(
    353       shim_path.Append("Contents").Append("MacOS").Append("app_mode_loader")));
    354 
    355   // Fix up the plist so that it matches the installed test app.
    356   NSString* plist_path = base::mac::FilePathToNSString(
    357       shim_path.Append("Contents").Append("Info.plist"));
    358   NSMutableDictionary* plist =
    359       [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];
    360 
    361   NSArray* keys_to_copy = @[
    362     base::mac::CFToNSCast(kCFBundleIdentifierKey),
    363     base::mac::CFToNSCast(kCFBundleNameKey),
    364     app_mode::kCrAppModeShortcutIDKey,
    365     app_mode::kCrAppModeUserDataDirKey,
    366     app_mode::kBrowserBundleIDKey
    367   ];
    368   for (NSString* key in keys_to_copy) {
    369     [plist setObject:[plist_64 objectForKey:key]
    370               forKey:key];
    371   }
    372   [plist writeToFile:plist_path
    373           atomically:YES];
    374 
    375   base::mac::RemoveQuarantineAttribute(shim_path);
    376 
    377   // Launch the shim, it should start the app and ultimately connect over IPC.
    378   // This actually happens in multiple launches of the shim:
    379   // (1) The shim will fail and instead launch Chrome with --app-id so that the
    380   //     app starts.
    381   // (2) Chrome launches the shim in response to an app starting, this time the
    382   //     shim launches Chrome with --app-shim-error, which causes Chrome to
    383   //     rebuild the shim.
    384   // (3) After rebuilding, Chrome again launches the shim and expects it to
    385   //     behave normally.
    386   ExtensionTestMessageListener launched_listener("Launched", false);
    387   CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
    388   ASSERT_TRUE(base::mac::OpenApplicationWithPath(
    389       shim_path, shim_cmdline, kLSLaunchDefaults, NULL));
    390 
    391   // Wait for the app to start (1). At this point there is no shim host.
    392   ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
    393   EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
    394 
    395   // Wait for the rebuilt shim to connect (3). This does not race with the app
    396   // starting (1) because Chrome only launches the shim (2) after the app
    397   // starts. Then Chrome must handle --app-shim-error on the UI thread before
    398   // the shim is rebuilt.
    399   WindowedAppShimLaunchObserver(app->id()).Wait();
    400 
    401   EXPECT_TRUE(GetFirstAppWindow());
    402   EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
    403 }
    404 
    405 #endif  // defined(ARCH_CPU_64_BITS)
    406 
    407 }  // namespace apps
    408