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