summaryrefslogtreecommitdiff
path: root/apps/app_shim/app_shim_interactive_uitest_mac.mm
blob: a51cc9bdc04c2bd994e34513eb91f22c7253d03a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
// 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.

#import <Cocoa/Cocoa.h>
#include <vector>

#include "apps/app_shim/app_shim_handler_mac.h"
#include "apps/app_shim/app_shim_host_manager_mac.h"
#include "apps/app_shim/extension_app_shim_handler_mac.h"
#include "apps/switches.h"
#include "apps/ui/native_app_window.h"
#include "base/auto_reset.h"
#include "base/callback.h"
#include "base/files/file_path_watcher.h"
#include "base/mac/foundation_util.h"
#include "base/mac/launch_services_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/apps/app_browsertest_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/extension_test_message_listener.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_mac.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/extension_registry.h"
#import "ui/events/test/cocoa_test_event_utils.h"

namespace {

// General end-to-end test for app shims.
class AppShimInteractiveTest : public extensions::PlatformAppBrowserTest {
 protected:
  AppShimInteractiveTest()
      : auto_reset_(&g_app_shims_allow_update_and_launch_in_tests, true) {}

 private:
  // Temporarily enable app shims.
  base::AutoReset<bool> auto_reset_;

  DISALLOW_COPY_AND_ASSIGN(AppShimInteractiveTest);
};

// Watches for changes to a file. This is designed to be used from the the UI
// thread.
class WindowedFilePathWatcher
    : public base::RefCountedThreadSafe<WindowedFilePathWatcher> {
 public:
  WindowedFilePathWatcher(const base::FilePath& path) : observed_(false) {
    content::BrowserThread::PostTask(
        content::BrowserThread::FILE,
        FROM_HERE,
        base::Bind(&WindowedFilePathWatcher::Watch, this, path));
  }

  void Wait() {
    if (observed_)
      return;

    run_loop_.reset(new base::RunLoop);
    run_loop_->Run();
  }

 protected:
  friend class base::RefCountedThreadSafe<WindowedFilePathWatcher>;
  virtual ~WindowedFilePathWatcher() {}

  void Watch(const base::FilePath& path) {
    watcher_.Watch(
        path, false, base::Bind(&WindowedFilePathWatcher::Observe, this));
  }

  void Observe(const base::FilePath& path, bool error) {
    content::BrowserThread::PostTask(
        content::BrowserThread::UI,
        FROM_HERE,
        base::Bind(&WindowedFilePathWatcher::StopRunLoop, this));
  }

  void StopRunLoop() {
    observed_ = true;
    if (run_loop_.get())
      run_loop_->Quit();
  }

 private:
  base::FilePathWatcher watcher_;
  bool observed_;
  scoped_ptr<base::RunLoop> run_loop_;

  DISALLOW_COPY_AND_ASSIGN(WindowedFilePathWatcher);
};

// Watches for an app shim to connect.
class WindowedAppShimLaunchObserver : public apps::AppShimHandler {
 public:
  WindowedAppShimLaunchObserver(const std::string& app_id)
      : app_mode_id_(app_id),
        observed_(false) {
    apps::AppShimHandler::RegisterHandler(app_id, this);
  }

  void Wait() {
    if (observed_)
      return;

    run_loop_.reset(new base::RunLoop);
    run_loop_->Run();
  }

  // AppShimHandler overrides:
  virtual void OnShimLaunch(Host* host,
                            apps::AppShimLaunchType launch_type,
                            const std::vector<base::FilePath>& files) OVERRIDE {
    // Remove self and pass through to the default handler.
    apps::AppShimHandler::RemoveHandler(app_mode_id_);
    apps::AppShimHandler::GetForAppMode(app_mode_id_)
        ->OnShimLaunch(host, launch_type, files);
    observed_ = true;
    if (run_loop_.get())
      run_loop_->Quit();
  }
  virtual void OnShimClose(Host* host) OVERRIDE {}
  virtual void OnShimFocus(Host* host,
                           apps::AppShimFocusType focus_type,
                           const std::vector<base::FilePath>& files) OVERRIDE {}
  virtual void OnShimSetHidden(Host* host, bool hidden) OVERRIDE {}
  virtual void OnShimQuit(Host* host) OVERRIDE {}

 private:
  std::string app_mode_id_;
  bool observed_;
  scoped_ptr<base::RunLoop> run_loop_;

  DISALLOW_COPY_AND_ASSIGN(WindowedAppShimLaunchObserver);
};

NSString* GetBundleID(const base::FilePath& shim_path) {
  base::FilePath plist_path = shim_path.Append("Contents").Append("Info.plist");
  NSMutableDictionary* plist = [NSMutableDictionary
      dictionaryWithContentsOfFile:base::mac::FilePathToNSString(plist_path)];
  return [plist objectForKey:base::mac::CFToNSCast(kCFBundleIdentifierKey)];
}

bool HasAppShimHost(Profile* profile, const std::string& app_id) {
  return g_browser_process->platform_part()
      ->app_shim_host_manager()
      ->extension_app_shim_handler()
      ->FindHost(profile, app_id);
}

}  // namespace

// Watches for NSNotifications from the shared workspace.
@interface WindowedNSNotificationObserver : NSObject {
 @private
  base::scoped_nsobject<NSString> bundleId_;
  BOOL notificationReceived_;
  scoped_ptr<base::RunLoop> runLoop_;
}

- (id)initForNotification:(NSString*)name
              andBundleId:(NSString*)bundleId;
- (void)observe:(NSNotification*)notification;
- (void)wait;
@end

@implementation WindowedNSNotificationObserver

- (id)initForNotification:(NSString*)name
              andBundleId:(NSString*)bundleId {
  if (self = [super init]) {
    bundleId_.reset([[bundleId copy] retain]);
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserver:self
           selector:@selector(observe:)
               name:name
             object:nil];
  }
  return self;
}

- (void)observe:(NSNotification*)notification {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  NSRunningApplication* application =
      [[notification userInfo] objectForKey:NSWorkspaceApplicationKey];
  if (![[application bundleIdentifier] isEqualToString:bundleId_])
    return;

  [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
  notificationReceived_ = YES;
  if (runLoop_.get())
    runLoop_->Quit();
}

- (void)wait {
  if (notificationReceived_)
    return;

  runLoop_.reset(new base::RunLoop);
  runLoop_->Run();
}

@end

namespace apps {

// Shims require static libraries http://crbug.com/386024.
#if defined(COMPONENT_BUILD)
#define MAYBE_Launch DISABLED_Launch
#define MAYBE_RebuildShim DISABLED_RebuildShim
#else
#define MAYBE_Launch Launch
#define MAYBE_RebuildShim RebuildShim
#endif

// Test that launching the shim for an app starts the app, and vice versa.
// These two cases are combined because the time to run the test is dominated
// by loading the extension and creating the shim.
IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_Launch) {
  // Install the app.
  const extensions::Extension* app = InstallPlatformApp("minimal");

  // Use a WebAppShortcutCreator to get the path.
  web_app::WebAppShortcutCreator shortcut_creator(
      web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
      web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
      extensions::FileHandlersInfo());
  base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
  EXPECT_FALSE(base::PathExists(shim_path));

  // Create the internal app shim by simulating an app update. FilePathWatcher
  // is used to wait for file operations on the shim to be finished before
  // attempting to launch it. Since all of the file operations are done in the
  // same event on the FILE thread, everything will be done by the time the
  // watcher's callback is executed.
  scoped_refptr<WindowedFilePathWatcher> file_watcher =
      new WindowedFilePathWatcher(shim_path);
  web_app::UpdateAllShortcuts(base::string16(), profile(), app);
  file_watcher->Wait();
  NSString* bundle_id = GetBundleID(shim_path);

  // Case 1: Launch the shim, it should start the app.
  {
    ExtensionTestMessageListener launched_listener("Launched", false);
    CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
    shim_cmdline.AppendSwitch(app_mode::kLaunchedForTest);
    ProcessSerialNumber shim_psn;
    ASSERT_TRUE(base::mac::OpenApplicationWithPath(
        shim_path, shim_cmdline, kLSLaunchDefaults, &shim_psn));
    ASSERT_TRUE(launched_listener.WaitUntilSatisfied());

    ASSERT_TRUE(GetFirstAppWindow());
    EXPECT_TRUE(HasAppShimHost(profile(), app->id()));

    // If the window is closed, the shim should quit.
    pid_t shim_pid;
    EXPECT_EQ(noErr, GetProcessPID(&shim_psn, &shim_pid));
    GetFirstAppWindow()->GetBaseWindow()->Close();
    ASSERT_TRUE(
        base::WaitForSingleProcess(shim_pid, TestTimeouts::action_timeout()));

    EXPECT_FALSE(GetFirstAppWindow());
    EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
  }

  // Case 2: Launch the app, it should start the shim.
  {
    base::scoped_nsobject<WindowedNSNotificationObserver> ns_observer;
    ns_observer.reset([[WindowedNSNotificationObserver alloc]
        initForNotification:NSWorkspaceDidLaunchApplicationNotification
                andBundleId:bundle_id]);
    WindowedAppShimLaunchObserver observer(app->id());
    LaunchPlatformApp(app);
    [ns_observer wait];
    observer.Wait();

    EXPECT_TRUE(GetFirstAppWindow());
    EXPECT_TRUE(HasAppShimHost(profile(), app->id()));

    // Quitting the shim will eventually cause it to quit. It actually
    // intercepts the -terminate, sends an AppShimHostMsg_QuitApp to Chrome,
    // and returns NSTerminateLater. Chrome responds by closing all windows of
    // the app. Once all windows are closed, Chrome closes the IPC channel,
    // which causes the shim to actually terminate.
    NSArray* running_shim = [NSRunningApplication
        runningApplicationsWithBundleIdentifier:bundle_id];
    ASSERT_EQ(1u, [running_shim count]);

    ns_observer.reset([[WindowedNSNotificationObserver alloc]
        initForNotification:NSWorkspaceDidTerminateApplicationNotification
                andBundleId:bundle_id]);
    [base::mac::ObjCCastStrict<NSRunningApplication>(
        [running_shim objectAtIndex:0]) terminate];
    [ns_observer wait];

    EXPECT_FALSE(GetFirstAppWindow());
    EXPECT_FALSE(HasAppShimHost(profile(), app->id()));
  }
}

#if defined(ARCH_CPU_64_BITS)

// Tests that a 32 bit shim attempting to launch 64 bit Chrome will eventually
// be rebuilt.
IN_PROC_BROWSER_TEST_F(AppShimInteractiveTest, MAYBE_RebuildShim) {
  // Get the 32 bit shim.
  base::FilePath test_data_dir;
  PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
  base::FilePath shim_path_32 =
      test_data_dir.Append("app_shim").Append("app_shim_32_bit.app");
  EXPECT_TRUE(base::PathExists(shim_path_32));

  // Install test app.
  const extensions::Extension* app = InstallPlatformApp("minimal");

  // Use WebAppShortcutCreator to create a 64 bit shim.
  web_app::WebAppShortcutCreator shortcut_creator(
      web_app::GetWebAppDataDirectory(profile()->GetPath(), app->id(), GURL()),
      web_app::ShortcutInfoForExtensionAndProfile(app, profile()),
      extensions::FileHandlersInfo());
  shortcut_creator.UpdateShortcuts();
  base::FilePath shim_path = shortcut_creator.GetInternalShortcutPath();
  NSMutableDictionary* plist_64 = [NSMutableDictionary
      dictionaryWithContentsOfFile:base::mac::FilePathToNSString(
          shim_path.Append("Contents").Append("Info.plist"))];

  // Copy 32 bit shim to where it's expected to be.
  // CopyDirectory doesn't seem to work when copying and renaming in one go.
  ASSERT_TRUE(base::DeleteFile(shim_path, true));
  ASSERT_TRUE(base::PathExists(shim_path.DirName()));
  ASSERT_TRUE(base::CopyDirectory(shim_path_32, shim_path.DirName(), true));
  ASSERT_TRUE(base::Move(shim_path.DirName().Append(shim_path_32.BaseName()),
                         shim_path));
  ASSERT_TRUE(base::PathExists(
      shim_path.Append("Contents").Append("MacOS").Append("app_mode_loader")));

  // Fix up the plist so that it matches the installed test app.
  NSString* plist_path = base::mac::FilePathToNSString(
      shim_path.Append("Contents").Append("Info.plist"));
  NSMutableDictionary* plist =
      [NSMutableDictionary dictionaryWithContentsOfFile:plist_path];

  NSArray* keys_to_copy = @[
    base::mac::CFToNSCast(kCFBundleIdentifierKey),
    base::mac::CFToNSCast(kCFBundleNameKey),
    app_mode::kCrAppModeShortcutIDKey,
    app_mode::kCrAppModeUserDataDirKey,
    app_mode::kBrowserBundleIDKey
  ];
  for (NSString* key in keys_to_copy) {
    [plist setObject:[plist_64 objectForKey:key]
              forKey:key];
  }
  [plist writeToFile:plist_path
          atomically:YES];

  base::mac::RemoveQuarantineAttribute(shim_path);

  // Launch the shim, it should start the app and ultimately connect over IPC.
  // This actually happens in multiple launches of the shim:
  // (1) The shim will fail and instead launch Chrome with --app-id so that the
  //     app starts.
  // (2) Chrome launches the shim in response to an app starting, this time the
  //     shim launches Chrome with --app-shim-error, which causes Chrome to
  //     rebuild the shim.
  // (3) After rebuilding, Chrome again launches the shim and expects it to
  //     behave normally.
  ExtensionTestMessageListener launched_listener("Launched", false);
  CommandLine shim_cmdline(CommandLine::NO_PROGRAM);
  ASSERT_TRUE(base::mac::OpenApplicationWithPath(
      shim_path, shim_cmdline, kLSLaunchDefaults, NULL));

  // Wait for the app to start (1). At this point there is no shim host.
  ASSERT_TRUE(launched_listener.WaitUntilSatisfied());
  EXPECT_FALSE(HasAppShimHost(profile(), app->id()));

  // Wait for the rebuilt shim to connect (3). This does not race with the app
  // starting (1) because Chrome only launches the shim (2) after the app
  // starts. Then Chrome must handle --app-shim-error on the UI thread before
  // the shim is rebuilt.
  WindowedAppShimLaunchObserver(app->id()).Wait();

  EXPECT_TRUE(GetFirstAppWindow());
  EXPECT_TRUE(HasAppShimHost(profile(), app->id()));
}

#endif  // defined(ARCH_CPU_64_BITS)

}  // namespace apps