Skip to content

Commit

Permalink
usb: Support device events when service worker is inactive
Browse files Browse the repository at this point in the history
This change list completes ServiceWorkerUsbDelegateObserver and makes it
responsible to keep track what ServiceWorkerRegistration should respond
to USB events and wake up the service worker if it is not in running
state.

Bug: 1446487
Change-Id: Ibadd754f302d6ab5eb42f7df0ecb46ba46226a7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4804134
Auto-Submit: Juan Garza Sanchez <juangarza@chromium.org>
Commit-Queue: Juan Garza Sanchez <juangarza@chromium.org>
Reviewed-by: Jack Hsieh <chengweih@chromium.org>
Commit-Queue: Jack Hsieh <chengweih@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1188062}
  • Loading branch information
Juan Garza Sanchez authored and Chromium LUCI CQ committed Aug 24, 2023
1 parent 029b6cb commit 1c7d8e8
Show file tree
Hide file tree
Showing 8 changed files with 725 additions and 27 deletions.
247 changes: 231 additions & 16 deletions chrome/browser/usb/chrome_usb_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/usb/chrome_usb_delegate.h"
#include "chrome/browser/usb/usb_browser_test_utils.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/browser/usb/usb_chooser_controller.h"
#include "chrome/browser/usb/usb_pinned_notification.h"
Expand All @@ -37,12 +38,17 @@
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/service_worker_running_info.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/usb_chooser.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
Expand Down Expand Up @@ -75,8 +81,19 @@ namespace {
using ::base::test::TestFuture;
using ::content::JsReplace;
using ::extensions::Extension;
using ::extensions::ExtensionId;
using ::extensions::TestExtensionDir;
using ::testing::Return;

const char kTestExtensionId[] = "iegclhlplifhodhkoafiokenjoapiobj";
// Key for extension id `kTestExtensionId`.
constexpr const char kTestExtensionKey[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P"
"Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf"
"qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv"
"7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i"
"L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz"
"afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB";
constexpr uint8_t kUsbPrinterClass = 7;
constexpr char kNonAppHost[] = "nonapp.com";
constexpr char kNonAppHost2[] = "nonapp2.com";
Expand All @@ -103,6 +120,83 @@ const AccountId kManagedUserAccountId =
AccountId::FromUserEmail("example@example.com");
#endif // BUILDFLAG(ENABLE_EXTENSIONS) && BUILDFLAG(IS_CHROMEOS_ASH)

// Observer for an extension service worker events like start, activated, and
// stop.
class TestServiceWorkerContextObserver
: public content::ServiceWorkerContextObserver {
public:
TestServiceWorkerContextObserver(content::ServiceWorkerContext* context,
const ExtensionId& extension_id)
: extension_url_(Extension::GetBaseURLFromExtensionId(extension_id)) {
scoped_observation_.Observe(context);
}

TestServiceWorkerContextObserver(const TestServiceWorkerContextObserver&) =
delete;
TestServiceWorkerContextObserver& operator=(
const TestServiceWorkerContextObserver&) = delete;

~TestServiceWorkerContextObserver() override = default;

void WaitForWorkerStart() {
started_run_loop_.Run();
EXPECT_TRUE(running_version_id_.has_value());
}

void WaitForWorkerActivated() {
activated_run_loop_.Run();
EXPECT_TRUE(running_version_id_.has_value());
}

void WaitForWorkerStop() {
stopped_run_loop_.Run();
EXPECT_EQ(running_version_id_, absl::nullopt);
}

int64_t GetServiceWorkerVersionId() { return running_version_id_.value(); }

private:
// ServiceWorkerContextObserver:
void OnVersionStartedRunning(
int64_t version_id,
const content::ServiceWorkerRunningInfo& running_info) override {
if (running_info.scope != extension_url_) {
return;
}
running_version_id_ = version_id;
started_run_loop_.Quit();
}

void OnVersionActivated(int64_t version_id, const GURL& scope) override {
if (running_version_id_ != version_id) {
return;
}
activated_run_loop_.Quit();
}

void OnVersionStoppedRunning(int64_t version_id) override {
if (running_version_id_ != version_id) {
return;
}
stopped_run_loop_.Quit();
running_version_id_ = absl::nullopt;
}

void OnDestruct(content::ServiceWorkerContext* context) override {
ASSERT_TRUE(scoped_observation_.IsObserving());
scoped_observation_.Reset();
}

base::RunLoop started_run_loop_;
base::RunLoop activated_run_loop_;
base::RunLoop stopped_run_loop_;
absl::optional<int64_t> running_version_id_;
base::ScopedObservation<content::ServiceWorkerContext,
content::ServiceWorkerContextObserver>
scoped_observation_{this};
GURL extension_url_;
};

class ChromeWebUsbTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
Expand Down Expand Up @@ -135,13 +229,13 @@ class ChromeWebUsbTest : public InProcessBrowserTest {
}

void AddFakeDevice(const std::string& serial_number) {
DCHECK(!fake_device_info_);
ASSERT_TRUE(!fake_device_info_);
fake_device_info_ = device_manager_.CreateAndAddDevice(
0, 0, "Test Manufacturer", "Test Device", serial_number);
}

void RemoveFakeDevice() {
DCHECK(fake_device_info_);
ASSERT_TRUE(fake_device_info_);
device_manager_.RemoveDevice(fake_device_info_->guid);
fake_device_info_ = nullptr;
}
Expand Down Expand Up @@ -935,28 +1029,34 @@ class WebUsbExtensionBrowserTest : public extensions::ExtensionBrowserTest {
kPolicyTemplate, extension->url().spec().c_str())));
}

const Extension* LoadExtensionAndRunTest(base::StringPiece background_js) {
constexpr char kManifestTemplate[] =
void SetUpTestDir(extensions::TestExtensionDir& test_dir,
base::StringPiece background_js) {
test_dir.WriteManifest(base::StringPrintf(
R"({
"name": "Test Extension",
"version": "0.1",
"manifest_version": 3,
"background": {
"service_worker": "%s"
}
})";
"name": "Test Extension",
"version": "0.1",
"key": "%s",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
})",
kTestExtensionKey));
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), background_js);
}

const Extension* LoadExtensionAndRunTest(base::StringPiece background_js) {
extensions::TestExtensionDir test_dir;
test_dir.WriteManifest(
base::StringPrintf(kManifestTemplate, "background.js"));
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), background_js);
SetUpTestDir(test_dir, background_js);

// Launch the test app.
ExtensionTestMessageListener ready_listener("ready",
ReplyBehavior::kWillReply);
extensions::ResultCatcher result_catcher;
const extensions::Extension* extension =
LoadExtension(test_dir.UnpackedPath());
CHECK(extension);
CHECK_EQ(extension->id(), kTestExtensionId);

// TODO(crbug.com/1336400): Grant permission using requestDevice().
// Run the test.
Expand All @@ -968,9 +1068,18 @@ class WebUsbExtensionBrowserTest : public extensions::ExtensionBrowserTest {
return extension;
}

device::FakeUsbDeviceManager& device_manager() { return device_manager_; }

void AddFakeDevice() {
device_manager_.CreateAndAddDevice(1234, 5678, "Test Manufacturer",
"Test Device", "123456");
DCHECK(!fake_device_info_);
fake_device_info_ = device_manager_.CreateAndAddDevice(
1234, 5678, "Test Manufacturer", "Test Device", "123456");
}

void RemoveFakeDevice() {
DCHECK(fake_device_info_);
device_manager_.RemoveDevice(fake_device_info_->guid);
fake_device_info_ = nullptr;
}

void SimulateClickOnSystemTrayIconButton(Browser* browser,
Expand Down Expand Up @@ -1034,6 +1143,7 @@ class WebUsbExtensionBrowserTest : public extensions::ExtensionBrowserTest {

private:
device::FakeUsbDeviceManager device_manager_;
device::mojom::UsbDeviceInfoPtr fake_device_info_;
#if BUILDFLAG(IS_CHROMEOS_ASH)
std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
#endif // BUILDFLAG(IS_CHROMEOS_ASH)
Expand Down Expand Up @@ -1146,6 +1256,111 @@ IN_PROC_BROWSER_TEST_F(WebUsbExtensionFeatureEnabledBrowserTest,
const auto* extension = LoadExtensionAndRunTest(kBackgroundJs);
SimulateClickOnSystemTrayIconButton(browser(), extension);
}

// Test the scenario of waking up the service worker upon device events and
// the service worker being kept alive with active device session.
IN_PROC_BROWSER_TEST_F(WebUsbExtensionFeatureEnabledBrowserTest,
DeviceConnectAndOpenDeviceWhenServiceWorkerStopped) {
content::ServiceWorkerContext* context = browser()
->profile()
->GetDefaultStoragePartition()
->GetServiceWorkerContext();
// Set up an observer for service worker events.
TestServiceWorkerContextObserver sw_observer(context, kTestExtensionId);

TestExtensionDir test_dir;
constexpr char kBackgroundJs[] = R"(
navigator.usb.onconnect = async (e) => {
chrome.test.sendMessage("connect", async () => {
try {
let device = e.device;
// Bounce device a few times to make sure nothing unexpected
// happens.
await device.open();
await device.close();
await device.open();
await device.close();
await device.open();
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
}
navigator.usb.ondisconnect = async (e) => {
chrome.test.sendMessage("disconnect", async () => {
try {
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
}
)";
SetUpTestDir(test_dir, kBackgroundJs);

// Launch the test app.
ExtensionTestMessageListener connect_listener("connect",
ReplyBehavior::kWillReply);
extensions::ResultCatcher result_catcher;
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
// TODO(crbug.com/1336400): Grant permission using requestDevice().
// Run the test.
SetUpPolicy(extension);
ASSERT_TRUE(extension);
ASSERT_EQ(extension->id(), kTestExtensionId);
sw_observer.WaitForWorkerStart();
sw_observer.WaitForWorkerActivated();

// The device event is handled right after the service worker is activated.
int64_t service_worker_version_id = sw_observer.GetServiceWorkerVersionId();
base::SimpleTestTickClock tick_clock;
AddFakeDevice();
EXPECT_TRUE(connect_listener.WaitUntilSatisfied());
connect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and the service worker is still alive due to active device
// session.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
// Since we have active USB device session at this point, click the USB system
// tray icon and check right links are opened by the browser.
SimulateClickOnSystemTrayIconButton(browser(), extension);

// Remove device will close the device session, and worker will stop running
// when it times out.
ExtensionTestMessageListener disconnect_listener("disconnect",
ReplyBehavior::kWillReply);
RemoveFakeDevice();
EXPECT_TRUE(disconnect_listener.WaitUntilSatisfied());
disconnect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and check that the receiver service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_FALSE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
sw_observer.WaitForWorkerStop();

// Another device event wakes up the inactive worker.
connect_listener.Reset();
AddFakeDevice();
EXPECT_TRUE(connect_listener.WaitUntilSatisfied());
connect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and the service worker is still alive due to active device
// session.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
// Since we have active USB device session at this point, click the USB system
// tray icon and check right links are opened by the browser.
SimulateClickOnSystemTrayIconButton(browser(), extension);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)

} // namespace

0 comments on commit 1c7d8e8

Please sign in to comment.