| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/test/bind.h" |
| #include "base/test/with_feature_override.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_browsertest.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "content/public/browser/service_worker_context.h" |
| #include "content/public/common/result_codes.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 "extensions/browser/disable_reason.h" |
| #include "extensions/browser/extension_registrar.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/service_worker/sequenced_context_id.h" |
| #include "extensions/browser/service_worker/service_worker_host.h" |
| #include "extensions/browser/service_worker/service_worker_state.h" |
| #include "extensions/browser/service_worker/service_worker_task_queue.h" |
| #include "extensions/browser/service_worker/service_worker_test_utils.h" |
| #include "extensions/browser/service_worker/worker_id_set.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/mojom/service_worker_host.mojom-test-utils.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "testing/gtest/include/gtest/gtest-spi.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/service_worker/service_worker_status_code.h" |
| #include "url/gurl.h" |
| |
| // Tests for validating the logic for keeping track of extension service |
| // workers. This is intentionally broad to include things like: |
| // * Starting and stopping state of the service worker |
| // * Keeping track of information for the running worker instance |
| |
| namespace extensions { |
| |
| namespace { |
| |
| using service_worker_test_utils::TestServiceWorkerTaskQueueObserver; |
| |
| // A helper class that intercepts the |
| // `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom receiver method and |
| // does *not* forward the call onto the real `ServiceWorkerHost` implementation. |
| class ServiceWorkerHostInterceptorForWorkerStop |
| : public mojom::ServiceWorkerHostInterceptorForTesting { |
| public: |
| // We use `worker_id` to have a weak handle to the `ServiceWorkerHost` |
| // since the host can be destroyed due to the worker stop in the test (the |
| // stop disconnects the mojom pipe and then destroys `ServiceWorkerHost`). |
| // Using the preferred `mojo::test::ScopedSwapImplForTesting()` would attempt |
| // to swap in a freed `ServiceWorkerHost*` when the test ends and cause a |
| // crash. |
| explicit ServiceWorkerHostInterceptorForWorkerStop(const WorkerId& worker_id) |
| : worker_id_(worker_id) { |
| auto* worker_host = extensions::ServiceWorkerHost::GetWorkerFor(worker_id_); |
| CHECK(worker_host) << "There is no ServiceWorkerHost for WorkerId: " |
| << worker_id_ << " when creating the stop interceptor."; |
| // Do not store a pointer `ServiceWorkerHost` to avoid lifetime issues, |
| // we'll use the `worker_id` as a weak handle instead. |
| std::ignore = worker_host->receiver_for_testing().SwapImplForTesting(this); |
| } |
| |
| mojom::ServiceWorkerHost* GetForwardingInterface() override { |
| // This should be non-null if this interface is still receiving events. This |
| // causes all methods other than `DidStopServiceWorkerContext()` to be sent |
| // along to the real implementation. |
| auto* worker_host = extensions::ServiceWorkerHost::GetWorkerFor(worker_id_); |
| CHECK(worker_host) << "There is no ServiceWorkerHost for WorkerId: " |
| << worker_id_ |
| << " when attempting to forward a mojom call to the " |
| "real `ServiceWorkerHost` implementation."; |
| return worker_host; |
| } |
| |
| protected: |
| // mojom::ServiceWorkerHost: |
| void DidStopServiceWorkerContext( |
| const ExtensionId& extension_id, |
| const base::UnguessableToken& activation_token, |
| const GURL& service_worker_scope, |
| int64_t service_worker_version_id, |
| int worker_thread_id) override { |
| // Do not call the real `ServiceWorkerHost::DidStopServiceWorkerContext()` |
| // method to simulate that a stop notification was never sent from the |
| // renderer worker thread. |
| } |
| |
| private: |
| const WorkerId worker_id_; |
| }; |
| |
| class ServiceWorkerTrackingBrowserTest : public ExtensionBrowserTest { |
| public: |
| ServiceWorkerTrackingBrowserTest() = default; |
| |
| ServiceWorkerTrackingBrowserTest(const ServiceWorkerTrackingBrowserTest&) = |
| delete; |
| ServiceWorkerTrackingBrowserTest& operator=( |
| const ServiceWorkerTrackingBrowserTest&) = delete; |
| |
| protected: |
| void TearDownOnMainThread() override { |
| ExtensionBrowserTest::TearDownOnMainThread(); |
| extension_ = nullptr; |
| } |
| |
| virtual std::string GetExtensionPageContent() const { return "<p>page</p>"; } |
| |
| void LoadServiceWorkerExtension() { |
| // Load a basic extension with a service worker and wait for the worker to |
| // start running. |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "Test Extension", |
| "manifest_version": 3, |
| "version": "0.1", |
| "background": { |
| "service_worker" : "background.js" |
| }, |
| "permissions": ["webNavigation"] |
| })"; |
| // The extensions script listens for runtime.onInstalled (to detect install |
| // and worker start completion) and webNavigation.onBeforeNavigate (to |
| // realistically request worker start). |
| static constexpr char kBackgroundScript[] = |
| R"( |
| chrome.runtime.onInstalled.addListener((details) => { |
| chrome.test.sendMessage('installed listener fired'); |
| }); |
| chrome.webNavigation.onBeforeNavigate.addListener((details) => { |
| chrome.test.sendMessage('listener fired'); |
| }); |
| )"; |
| auto test_dir = std::make_unique<TestExtensionDir>(); |
| test_dir->WriteManifest(kManifest); |
| test_dir->WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundScript); |
| test_dir->WriteFile(FILE_PATH_LITERAL("extension_page_tab.html"), |
| GetExtensionPageContent()); |
| ExtensionTestMessageListener extension_oninstall_listener_fired( |
| "installed listener fired"); |
| const Extension* extension = LoadExtension( |
| test_dir->UnpackedPath(), {.wait_for_registration_stored = true}); |
| test_extension_dirs_.push_back(std::move(test_dir)); |
| ASSERT_TRUE(extension); |
| extension_ = extension; |
| ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied()); |
| |
| // Verify the worker is running. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| ASSERT_TRUE(worker_state); |
| const std::optional<WorkerId>& worker_id = worker_state->worker_id(); |
| ASSERT_TRUE(worker_id.has_value()); |
| ASSERT_TRUE(content::CheckServiceWorkerIsRunning(GetServiceWorkerContext(), |
| worker_id->version_id)); |
| } |
| |
| ServiceWorkerState* GetWorkerState() { |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| std::optional<base::UnguessableToken> activation_token = |
| task_queue->GetCurrentActivationToken(extension_->id()); |
| if (!activation_token) { |
| return nullptr; |
| } |
| SequencedContextId context_id{extension_->id(), profile()->UniqueId(), |
| activation_token.value()}; |
| return task_queue->GetWorkerStateForTesting(context_id); |
| } |
| |
| const Extension* extension() { return extension_; } |
| |
| TestExtensionDir* test_extension_dir() { |
| if (test_extension_dirs_.size() != 1) { |
| ADD_FAILURE() << "Expected exactly one test extension directory"; |
| return nullptr; |
| } |
| return test_extension_dirs_.front().get(); |
| } |
| |
| raw_ptr<const Extension> extension_; |
| // Ensure `TestExtensionDir`s live past the test helper methods finishing. |
| std::vector<std::unique_ptr<TestExtensionDir>> test_extension_dirs_; |
| }; |
| |
| // Test class to help verify the tracking of `WorkerId`s in |
| // `ServiceWorkerTaskQueue` and `WorkerIdSet`. |
| class ServiceWorkerIdTrackingBrowserTest |
| : public ServiceWorkerTrackingBrowserTest { |
| public: |
| ServiceWorkerIdTrackingBrowserTest() |
| // Prevent the test from hitting CHECKs so we can examine `WorkerIdSet` at |
| // the end of the tests. |
| : allow_multiple_worker_per_extension_in_worker_id_set_( |
| WorkerIdSet::AllowMultipleWorkersPerExtensionForTesting()), |
| allow_multiple_workers_per_extension_in_task_queue_( |
| ServiceWorkerState::AllowMultipleWorkersPerExtensionForTesting()) {} |
| |
| protected: |
| void SetUpOnMainThread() override { |
| ServiceWorkerTrackingBrowserTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| process_manager_ = ProcessManager::Get(profile()); |
| ASSERT_TRUE(process_manager_); |
| } |
| |
| void TearDownOnMainThread() override { |
| ServiceWorkerTrackingBrowserTest::TearDownOnMainThread(); |
| process_manager_ = nullptr; |
| } |
| |
| void OpenExtensionTab() { |
| // Load a page from a resource inside the extension (and therefore inside |
| // the extension render process). This prevents the //content layer from |
| // completely shutting down the render process (which is another way that |
| // eventually removes the worker from `WorkerIdSet`). |
| SCOPED_TRACE("Loading extension tab for test extension"); |
| NavigateToURLInNewTab( |
| extension_->GetResourceURL("extension_page_tab.html")); |
| } |
| |
| void LoadServiceWorkerExtensionAndOpenExtensionTab() { |
| LoadServiceWorkerExtension(); |
| OpenExtensionTab(); |
| } |
| |
| std::optional<WorkerId> GetWorkerIdForExtension() { |
| std::vector<WorkerId> service_workers_for_extension = |
| process_manager_->GetServiceWorkersForExtension(extension()->id()); |
| if (service_workers_for_extension.size() > 1u) { |
| ADD_FAILURE() << "Expected only one worker for extension: " |
| << extension()->id() |
| << " But found incorrect number of workers: " |
| << service_workers_for_extension.size(); |
| return std::nullopt; |
| } |
| return service_workers_for_extension.empty() |
| ? std::nullopt |
| : std::optional<WorkerId>(service_workers_for_extension[0]); |
| } |
| |
| // Starts the worker and waits for the worker to initialize. |
| void StartWorker() { |
| // Confirm the worker for the extension does not appear to be running, |
| // otherwise this will hang forever. |
| std::optional<WorkerId> worker_id = GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id); |
| |
| // Add an observer to the task queue to detect when the new worker instance |
| // `WorkerId` is added to `WorkerIdSet`. |
| TestServiceWorkerTaskQueueObserver worker_id_added_observer; |
| |
| // Navigate somewhere to trigger the start of the worker to handle the |
| // webNavigation.onBeforeRequest event. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), |
| embedded_test_server()->GetURL("example.com", "/simple.html"))); |
| |
| // Wait for the new worker instance to be added to `WorkerIdSet` (registered |
| // in the process manager). |
| SCOPED_TRACE( |
| "Waiting for worker to restart in response to extensions event."); |
| worker_id_added_observer.WaitForWorkerContextInitialized(extension()->id()); |
| } |
| |
| |
| private: |
| raw_ptr<ProcessManager> process_manager_; |
| base::AutoReset<bool> allow_multiple_worker_per_extension_in_worker_id_set_; |
| base::AutoReset<bool> allow_multiple_workers_per_extension_in_task_queue_; |
| }; |
| |
| // TODO(crbug.com/40936639): improve the stall test by using similar logic to |
| // ServiceWorkerVersionTest.StallInStopping_DetachThenStart to more closely |
| // simulate a worker thread delayed in stopping. This will also allow testing |
| // when the delay causes ProcessManager::RenderProcessExited() to be called |
| // before ServiceWorkerState::OnStoppedSync(). |
| |
| // Tests that when: |
| // 1) something, other than a worker, keeps the extension renderer process |
| // alive (e.g. a tab is open to a page hosted inside the extension) and |
| // 2) simultaneously the worker is stopped but is stalled/blocked in |
| // terminating (preventing notification to //extensions that it has stopped) |
| // and |
| // 3) sometime later a new worker instance is started (e.g. by a new extension |
| // event that is sent) |
| // |
| // (a.k.a a "delayed worker stop") the //extensions browser layer should only |
| // track (`WorkerIdSet`) one worker instance (`WorkerId`) (the new worker |
| // instance). This avoids tracking one or more instances of stopped workers. |
| // Regression test for crbug.com/40936639. |
| IN_PROC_BROWSER_TEST_F( |
| ServiceWorkerIdTrackingBrowserTest, |
| WorkerStalledInStopping_RemovedByBrowserStopNotification) { |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtensionAndOpenExtensionTab()); |
| |
| // Get the soon to be stopped ("previous") worker's `WorkerId`. |
| std::optional<WorkerId> previous_service_worker_id = |
| GetWorkerIdForExtension(); |
| ASSERT_TRUE(previous_service_worker_id); |
| |
| // Setup intercept of `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom |
| // call. This simulates the worker renderer thread being very slow/never |
| // informing the //extensions browser layer that the worker context/thread |
| // terminated. |
| ServiceWorkerHostInterceptorForWorkerStop stop_interceptor( |
| *previous_service_worker_id); |
| |
| // Stop the service worker. Note: despite the worker actually terminating in |
| // the test, `stop_interceptor` has intercepted and prevented the stop |
| // notification from occurring which prevents the previous worker instance |
| // from being removed from `WorkerIdSet`. Combined with the open extension tab |
| // above the worker is simulated as being stalled/blocked in terminating. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension()->id()); |
| ASSERT_TRUE(content::CheckServiceWorkerIsStopped( |
| GetServiceWorkerContext(), previous_service_worker_id->version_id)); |
| |
| // Confirm after stopping we no longer have the previous `WorkerId`. The |
| // browser stop notification should've removed it for us because the renderer |
| // stop never happened. |
| std::optional<WorkerId> worker_id_after_stop_worker = |
| GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id_after_stop_worker); |
| |
| // Start the new instance of the worker and wait for it to start. |
| ASSERT_NO_FATAL_FAILURE(StartWorker()); |
| |
| // Confirm that we are only tracking one running worker. |
| std::optional<WorkerId> newly_started_service_worker_id = |
| GetWorkerIdForExtension(); |
| ASSERT_TRUE(newly_started_service_worker_id); |
| |
| // Confirm `WorkerId` being tracked seems to be a different started instance |
| // than the first one (WorkerIds are sorted by their attributes so the last is |
| // considered the newest WorkerId since it has a higher thread, or process id, |
| // etc.). |
| // TODO(jlulejian): Is there a less fragile way of confirming this? If the |
| // same render process uses the same thread ID this would then fail. |
| EXPECT_NE(newly_started_service_worker_id, previous_service_worker_id); |
| } |
| |
| // Test that when a worker is stopped and then restarted we only track one |
| // instance of `WorkerId` in `WorkerIdSet`. This specific test removes it via |
| // the renderer stop notification first (but it could also happen in other ways) |
| // and then ensures the browser stop notification doesn't try to doubly remove |
| // the `WorkerId`. |
| IN_PROC_BROWSER_TEST_F( |
| ServiceWorkerIdTrackingBrowserTest, |
| WorkerNotStalledInStopping_RemovedByRenderStopNotificationFirst) { |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtensionAndOpenExtensionTab()); |
| |
| // Get the soon to be stopped ("previous") worker's information. |
| std::optional<WorkerId> previous_service_worker_id = |
| GetWorkerIdForExtension(); |
| ASSERT_TRUE(previous_service_worker_id); |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| ASSERT_TRUE(sw_context); |
| ASSERT_TRUE(base::Contains(sw_context->GetRunningServiceWorkerInfos(), |
| previous_service_worker_id->version_id)); |
| const content::ServiceWorkerRunningInfo& sw_info = |
| sw_context->GetRunningServiceWorkerInfos().at( |
| previous_service_worker_id->version_id); |
| // Remove the worker state as an observer of `ServiceWorkerContext` so that |
| // the browser stop notification will not run immediately. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| worker_state->StopObservingContextForTest(); |
| |
| TestServiceWorkerTaskQueueObserver worker_id_removed_observer; |
| |
| // Stop the service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension()->id()); |
| ASSERT_TRUE(content::CheckServiceWorkerIsStopped( |
| sw_context, previous_service_worker_id->version_id)); |
| |
| worker_id_removed_observer.WaitForWorkerStopped(extension()->id()); |
| |
| // Confirm after stopping we no longer have the previous `WorkerId` (it was |
| // removed by the renderer stop notification). |
| std::optional<WorkerId> worker_id_after_stop_worker_renderer = |
| GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id_after_stop_worker_renderer); |
| |
| // Run the browser stop notification after the renderer stop notification, and |
| // it should do nothing. |
| worker_state->OnStoppedSync(previous_service_worker_id->version_id, |
| sw_info.scope); |
| |
| // Confirm after the browser stop notification that we are still no longer |
| // tracking the worker. |
| std::optional<WorkerId> worker_id_after_stop_worker_browser = |
| GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id_after_stop_worker_browser); |
| } |
| |
| // Test that when a worker is stopped and then restarted we only track one |
| // instance of `WorkerId` in `WorkerIdSet`. This specific test removes it via |
| // the browser stop notification first and then ensures the renderer stop |
| // notification doesn't try to doubly remove the `WorkerId`. |
| IN_PROC_BROWSER_TEST_F( |
| ServiceWorkerIdTrackingBrowserTest, |
| WorkerNotStalledInStopping_RemovedByBrowserStopNotificationFirst) { |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtensionAndOpenExtensionTab()); |
| |
| // Get the soon to be stopped ("previous") worker's `WorkerId`. |
| std::optional<WorkerId> previous_service_worker_id = |
| GetWorkerIdForExtension(); |
| ASSERT_TRUE(previous_service_worker_id); |
| |
| // Get the activation token for later passing to the render stop notification. |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| ASSERT_TRUE(task_queue); |
| auto activation_token = |
| task_queue->GetCurrentActivationToken(extension()->id()); |
| ASSERT_TRUE(activation_token); |
| |
| // Setup intercept of `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom |
| // call. This simulates the worker renderer thread being very slow/never |
| // informing the //extensions browser layer that the worker context/thread |
| // terminated. |
| ServiceWorkerHostInterceptorForWorkerStop stop_interceptor( |
| *previous_service_worker_id); |
| |
| // Stop the service worker. Note: despite the worker actually terminating in |
| // the test, `stop_interceptor` has intercepted and prevented the stop |
| // notification from occurring which prevents the previous worker instance |
| // from being removed from `WorkerIdSet`. Combined with the open extension tab |
| // above the worker is simulated as being stalled/blocked in terminating. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope( |
| profile(), previous_service_worker_id->extension_id); |
| ASSERT_TRUE(content::CheckServiceWorkerIsStopped( |
| GetServiceWorkerContext(), previous_service_worker_id->version_id)); |
| |
| // Confirm after stopping we no longer have the previous `WorkerId`. The |
| // browser stop notification should've removed it for us. |
| std::optional<WorkerId> worker_id_after_stop_worker_browser = |
| GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id_after_stop_worker_browser); |
| |
| // TODO(crbug.com/40936639): test this with `ServiceWorkerHost` rather than |
| // `ServiceWorkerTaskQueue` once we can mimic the stalling situation |
| // precisely. As-is these tests actually stop the render which destroys |
| // `ServiceWorkerHost`. |
| // "Send" the render stop notification second |
| task_queue->RendererDidStopServiceWorkerContext( |
| previous_service_worker_id->render_process_id, |
| previous_service_worker_id->extension_id, activation_token.value(), |
| /*service_worker_scope=*/extension()->url(), |
| previous_service_worker_id->version_id, |
| previous_service_worker_id->thread_id); |
| |
| // Confirm after the renderer stop notification we still no longer have the |
| // previous `WorkerId`. |
| std::optional<WorkerId> worker_id_after_stop_worker_renderer = |
| GetWorkerIdForExtension(); |
| ASSERT_EQ(std::nullopt, worker_id_after_stop_worker_renderer); |
| } |
| |
| using ServiceWorkerStopTrackingBrowserTest = ServiceWorkerTrackingBrowserTest; |
| |
| class ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart |
| : public ServiceWorkerStopTrackingBrowserTest, |
| public base::test::WithFeatureOverride { |
| public: |
| ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart() |
| : WithFeatureOverride( |
| extensions_features::kOptimizeServiceWorkerStartRequests) {} |
| }; |
| |
| // Test that if a browser stop notification is received before the render stop |
| // notification (since these things can be triggered independently) the worker's |
| // browser and renderer state are both set to not ready. |
| IN_PROC_BROWSER_TEST_P( |
| ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart, |
| OnStoppedUpdatesBrowserAndRendererState_BeforeRenderStopNotification) { |
| const bool wakeup_optimization_enabled = IsParamFeatureEnabled(); |
| const auto kExpectedBrowserState = |
| wakeup_optimization_enabled ? ServiceWorkerState::BrowserState::kActive |
| : ServiceWorkerState::BrowserState::kReady; |
| |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtension()); |
| |
| // Get information about worker for extension that will be stopped soon. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| ASSERT_TRUE(worker_state); |
| std::optional<WorkerId> stopped_service_worker_id = worker_state->worker_id(); |
| ASSERT_TRUE(stopped_service_worker_id); |
| |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| ASSERT_TRUE(task_queue); |
| // Confirm the worker is browser state ready. |
| std::optional<base::UnguessableToken> activation_token = |
| task_queue->GetCurrentActivationToken(extension()->id()); |
| ASSERT_TRUE(activation_token); |
| ASSERT_EQ(worker_state->browser_state(), kExpectedBrowserState); |
| |
| // Setup intercept of `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom |
| // call. This simulates the worker renderer thread being very slow/never |
| // informing the //extensions browser layer that the worker context/thread |
| // terminated. |
| ServiceWorkerHostInterceptorForWorkerStop stop_interceptor( |
| *stopped_service_worker_id); |
| |
| // Stop the service worker. Note: despite the worker actually terminating in |
| // the test, `stop_interceptor` has intercepted and prevented the render stop |
| // notification from occurring. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope( |
| profile(), stopped_service_worker_id->extension_id); |
| ASSERT_TRUE(content::CheckServiceWorkerIsStopped( |
| GetServiceWorkerContext(), stopped_service_worker_id->version_id)); |
| |
| // Confirm the worker state does still exist, and that the browser stop |
| // notification reset it to no longer ready. |
| EXPECT_EQ(worker_state->browser_state(), |
| ServiceWorkerState::BrowserState::kNotActive); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kNotActive); |
| |
| // Confirm the worker has been untracked from ProcessManager. |
| std::vector<WorkerId> workers_for_extension = |
| ProcessManager::Get(profile())->GetServiceWorkersForExtension( |
| extension()->id()); |
| EXPECT_EQ(workers_for_extension.size(), 0ul); |
| |
| // Simulate the render stop notification arriving afterwards. |
| task_queue->RendererDidStopServiceWorkerContext( |
| stopped_service_worker_id->render_process_id, |
| stopped_service_worker_id->extension_id, activation_token.value(), |
| /*service_worker_scope=*/extension()->url(), |
| stopped_service_worker_id->version_id, |
| stopped_service_worker_id->thread_id); |
| |
| // Confirm the worker state still exists and state remains the same. |
| EXPECT_EQ(worker_state->browser_state(), |
| ServiceWorkerState::BrowserState::kNotActive); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kNotActive); |
| } |
| |
| // Test that if a browser stop notification is received after the render stop |
| // notification (since these things can be triggered independently) |
| // the worker's browser and renderer readiness information remains not ready. |
| IN_PROC_BROWSER_TEST_P( |
| ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart, |
| OnStoppedUpdatesBrowserAndRendererState_AfterRenderStopNotification) { |
| const bool wakeup_optimization_enabled = IsParamFeatureEnabled(); |
| const auto kExpectedBrowserState = |
| wakeup_optimization_enabled ? ServiceWorkerState::BrowserState::kActive |
| : ServiceWorkerState::BrowserState::kReady; |
| |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtension()); |
| |
| // Get information about worker for extension that will be stopped soon. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| ASSERT_TRUE(worker_state); |
| std::optional<WorkerId> stopped_service_worker_id = worker_state->worker_id(); |
| ASSERT_TRUE(stopped_service_worker_id); |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| ASSERT_TRUE(sw_context); |
| ASSERT_TRUE(base::Contains(sw_context->GetRunningServiceWorkerInfos(), |
| stopped_service_worker_id->version_id)); |
| const content::ServiceWorkerRunningInfo& sw_info = |
| sw_context->GetRunningServiceWorkerInfos().at( |
| stopped_service_worker_id->version_id); |
| |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| ASSERT_TRUE(task_queue); |
| // Confirm the worker is browser state ready. |
| ASSERT_EQ(worker_state->browser_state(), kExpectedBrowserState); |
| |
| // Remove the worker state as an observer of `ServiceWorkerContext` so that |
| // the browser stop notification will not run immediately. |
| worker_state->StopObservingContextForTest(); |
| |
| // Stop the service worker. |
| browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(), |
| extension()->id()); |
| ASSERT_TRUE(content::CheckServiceWorkerIsStopped( |
| sw_context, stopped_service_worker_id->version_id)); |
| |
| // Confirm the worker state still exists and browser and renderer state are |
| // not ready. |
| EXPECT_EQ(worker_state->browser_state(), |
| ServiceWorkerState::BrowserState::kNotActive); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kNotActive); |
| |
| // Simulate browser stop notification after the render stop notification. |
| worker_state->OnStoppedSync(stopped_service_worker_id->version_id, |
| sw_info.scope); |
| |
| // Confirm the worker state still exists, and browser and renderer state |
| // remain not ready. |
| EXPECT_EQ(worker_state->browser_state(), |
| ServiceWorkerState::BrowserState::kNotActive); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kNotActive); |
| |
| // Confirm the worker has been untracked from ProcessManager. |
| std::vector<WorkerId> workers_for_extension = |
| ProcessManager::Get(profile())->GetServiceWorkersForExtension( |
| extension()->id()); |
| EXPECT_EQ(workers_for_extension.size(), 0ul); |
| } |
| |
| // Test that if an extension and its worker are deactivated, the worker is |
| // untracked from both ServiceWorkerTaskQueue and ProcessManager. |
| IN_PROC_BROWSER_TEST_P( |
| ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart, |
| DisablingExtensionUntracksWorker) { |
| const bool wakeup_optimization_enabled = IsParamFeatureEnabled(); |
| const auto kExpectedBrowserState = |
| wakeup_optimization_enabled ? ServiceWorkerState::BrowserState::kActive |
| : ServiceWorkerState::BrowserState::kReady; |
| |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtension()); |
| |
| // Get information about worker for extension that will be deactivated soon. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| ASSERT_TRUE(worker_state); |
| std::optional<WorkerId> deactivated_service_worker_id = |
| worker_state->worker_id(); |
| ASSERT_TRUE(deactivated_service_worker_id); |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| ASSERT_TRUE(sw_context); |
| ASSERT_TRUE(base::Contains(sw_context->GetRunningServiceWorkerInfos(), |
| deactivated_service_worker_id->version_id)); |
| |
| // Confirm the worker is browser state ready. |
| ASSERT_EQ(worker_state->browser_state(), kExpectedBrowserState); |
| |
| // Deactivate extension. |
| extensions::ExtensionRegistrar::Get(profile())->DisableExtension( |
| extension()->id(), {disable_reason::DISABLE_USER_ACTION}); |
| |
| // Confirm the worker state does not exist. |
| worker_state = GetWorkerState(); |
| ASSERT_FALSE(worker_state); |
| |
| // Confirm the worker has been untracked from ProcessManager. |
| std::vector<WorkerId> workers_for_extension = |
| ProcessManager::Get(profile())->GetServiceWorkersForExtension( |
| extension()->id()); |
| EXPECT_EQ(workers_for_extension.size(), 0ul); |
| } |
| |
| // Toggle `extensions_features::OptimizeServiceWorkerStartRequests`. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE( |
| ServiceWorkerStopTrackingBrowserTestWithOptimizeServiceWorkerStart); |
| |
| // Test that if a renderer process exit notification is received before |
| // a browser stop notification (since these things can be triggered |
| // independently) and a context stop notification, it updates the worker's |
| // browser and renderer active state to inactive. |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerStopTrackingBrowserTest, |
| RenderProcessExitedUpdatesBrowserAndRendererState) { |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtension()); |
| |
| // Get information about worker for extension that will be stopped soon. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| ASSERT_TRUE(worker_state); |
| std::optional<WorkerId> worker_id = worker_state->worker_id(); |
| ASSERT_TRUE(worker_id); |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| ASSERT_TRUE(sw_context); |
| ASSERT_TRUE(base::Contains(sw_context->GetRunningServiceWorkerInfos(), |
| worker_id->version_id)); |
| |
| ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile()); |
| ASSERT_TRUE(task_queue); |
| // Confirm the worker is renderer state active. |
| ASSERT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kActive); |
| |
| // Remove the worker state as an observer of `ServiceWorkerContext` so that |
| // the browser stop notification will not run immediately. |
| worker_state->StopObservingContextForTest(); |
| // Setup intercept of `ServiceWorkerHost::DidStopServiceWorkerContext()`. |
| // This simulates the worker renderer thread never informing that the worker |
| // context terminated. |
| ServiceWorkerHostInterceptorForWorkerStop stop_interceptor(*worker_id); |
| |
| // Kill the service worker's renderer. |
| content::RenderProcessHost* worker_render_process_host = |
| content::RenderProcessHost::FromID(worker_id->render_process_id); |
| ASSERT_TRUE(worker_render_process_host); |
| content::RenderProcessHostWatcher process_exit_observer( |
| worker_render_process_host, |
| content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); |
| worker_render_process_host->Shutdown(content::RESULT_CODE_KILLED); |
| process_exit_observer.Wait(); |
| |
| // Verify the service worker was stopped. |
| ASSERT_TRUE( |
| content::CheckServiceWorkerIsStopped(sw_context, worker_id->version_id)); |
| |
| // Confirm the worker state still exists and browser and renderer states have |
| // been set to inactive by `ServiceWorkerHost::RenderProcessForWorkerExited`. |
| EXPECT_EQ(worker_state->browser_state(), |
| ServiceWorkerState::BrowserState::kNotActive); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kNotActive); |
| } |
| |
| using ServiceWorkerRendererTrackingBrowserTest = ExtensionApiTest; |
| |
| // Tests that when reloading an extension that has a worker to a version of the |
| // extension that doesn't have a worker, we don't persist the worker activation |
| // token in the renderer across extension loads/unloads. |
| // Regression test for crbug.com/372753069. |
| // TODO(crbug.com/372753069): Duplicate this test for extension updates. |
| IN_PROC_BROWSER_TEST_F(ServiceWorkerRendererTrackingBrowserTest, |
| UnloadingExtensionClearsRendererActivationToken) { |
| embedded_test_server()->ServeFilesFromSourceDirectory(GetChromeTestDataDir()); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| // Initial version has a worker. |
| static constexpr char kManifestWithWorker[] = |
| R"({ |
| "name": "Test extension", |
| "manifest_version": 3, |
| "version": "0.1", |
| "background": {"service_worker": "background.js"} |
| })"; |
| static constexpr char kWorkerBackground[] = |
| R"(chrome.test.sendMessage('ready');)"; |
| // New version no longer has a worker (it adds a content script only for test |
| // waiting purposes). |
| static constexpr char kManifestWithoutWorker[] = |
| R"({ |
| "name": "Test extension", |
| "manifest_version": 3, |
| "version": "0.2", |
| "content_scripts": [{ |
| "matches": ["<all_urls>"], |
| "js": ["script.js"], |
| "all_frames": true, |
| "run_at": "document_start" |
| }] |
| })"; |
| static constexpr char kContentScript[] = |
| R"(chrome.test.sendMessage('script injected');)"; |
| TestExtensionDir extension_dir; |
| extension_dir.WriteManifest(kManifestWithWorker); |
| extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), |
| kWorkerBackground); |
| extension_dir.WriteFile(FILE_PATH_LITERAL("script.js"), kContentScript); |
| |
| // Install initial version of the extension with a worker. |
| const Extension* extension = nullptr; |
| ExtensionTestMessageListener listener("ready"); |
| { |
| // This installation will populate a worker activation token in the |
| // extension renderer for the worker. |
| SCOPED_TRACE("installing extension with a worker"); |
| extension = LoadExtension(extension_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| } |
| |
| // By waiting for the worker to be started and receive an event, we indirectly |
| // can be fairly certain that the renderer has loaded the extension and |
| // populated the worker activation token. |
| { |
| SCOPED_TRACE("waiting extension with worker's background script to start"); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| const ExtensionId original_extension_id = extension->id(); |
| |
| // Reload to the new version of the extension without a worker. |
| extension_dir.WriteManifest(kManifestWithoutWorker); |
| |
| ExtensionTestMessageListener extension_without_worker_loaded( |
| "script injected"); |
| |
| // Reload the extension so it no longer has a worker. |
| { |
| SCOPED_TRACE( |
| "reloading extension with a worker to new version without a worker"); |
| // Reloading the extension should unload the original version of the |
| // extension which will remove the worker activation token in the renderer. |
| // Then the subsequent load will load the new version of the extension |
| // without a worker activation token. The bug was that the token was never |
| // removed and would remain across this reload. We CHECK() that non-worker |
| // based extension do not have activation tokens which would've crash the |
| // renderer prior to the fix. |
| ReloadExtension(original_extension_id); |
| } |
| |
| // To indirectly confirm that the extension without a worker loaded in the |
| // renderer we navigate to a page and wait for the content script to run. |
| ASSERT_TRUE(ui_test_utils::NavigateToURL( |
| browser(), embedded_test_server()->GetURL("/extensions/test_file.html"))); |
| { |
| SCOPED_TRACE("waiting for extension without worker to load"); |
| ASSERT_TRUE(extension_without_worker_loaded.WaitUntilSatisfied()); |
| } |
| |
| // Confirm the extension updated to the new version without a worker. |
| const Extension* new_extension_version = |
| ExtensionRegistry::Get(profile())->GetInstalledExtension( |
| original_extension_id); |
| ASSERT_TRUE(new_extension_version); |
| ASSERT_EQ("0.2", new_extension_version->version().GetString()); |
| |
| // Double-confirm that after our wait the renderer hasn't crashed. |
| content::WebContents* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(web_contents); |
| EXPECT_FALSE(web_contents->IsCrashed()); |
| } |
| |
| // Tests tracking behavior of the main extension service worker when an |
| // additional service worker is registered by the extension for a sub-scope |
| // via `navigator.serviceWorker.register()` from an extension page. |
| class |
| ServiceWorkerSubScopeWorkerTrackingBrowserTestWithOptimizeServiceWorkerStart |
| : public ServiceWorkerIdTrackingBrowserTest, |
| public base::test::WithFeatureOverride { |
| public: |
| ServiceWorkerSubScopeWorkerTrackingBrowserTestWithOptimizeServiceWorkerStart() |
| : WithFeatureOverride( |
| extensions_features::kOptimizeServiceWorkerStartRequests) {} |
| |
| protected: |
| std::string GetExtensionPageContent() const override { |
| return R"(<script src="/page.js"></script>)"; |
| } |
| |
| void LoadSubScopeServiceWorker() { |
| // Code for a service worker that will be registered for a sub-scope |
| // of the extension root scope. This service worker is not allowed |
| // access to extension APIs, as it's not listed in the manifest. |
| { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| base::CreateDirectory(test_extension_dir()->UnpackedPath().Append( |
| FILE_PATH_LITERAL("subscope"))); |
| test_extension_dir()->WriteFile(FILE_PATH_LITERAL("subscope/sw.js"), R"( |
| console.log("subscope service worker"); |
| )"); |
| } |
| |
| // Code for the script that will be executed as part of the extension page. |
| // This registers the previously defined service worker. |
| test_extension_dir()->WriteFile(FILE_PATH_LITERAL("page.js"), R"( |
| navigator.serviceWorker.register("subscope/sw.js").then(function() { |
| // Wait until the service worker is active. |
| return navigator.serviceWorker.ready; |
| }).catch(function(err) { |
| console.log("registration error: " + err.message); |
| }); |
| )"); |
| |
| // Open the extension page, which will cause the sub-scope service |
| // worker to start. We wait for its registration here. |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| service_worker_test_utils::TestServiceWorkerContextObserver |
| registration_observer(sw_context); |
| OpenExtensionTab(); |
| registration_observer.WaitForRegistrationStored(); |
| } |
| }; |
| |
| // Tests that stopping a service worker that was registered for |
| // a sub-scope via `navigation.serviceWorker.register()`, rather |
| // than being declared in the extension's manifest does not influence the |
| // tracking of the main extension service worker. Regression test for |
| // crbug.com/395536907. |
| IN_PROC_BROWSER_TEST_P( |
| ServiceWorkerSubScopeWorkerTrackingBrowserTestWithOptimizeServiceWorkerStart, |
| StoppingSubScopeWorkerDoesNotAffectExtensionWorker) { |
| const bool wakeup_optimization_enabled = IsParamFeatureEnabled(); |
| const auto kExpectedBrowserState = |
| wakeup_optimization_enabled ? ServiceWorkerState::BrowserState::kActive |
| : ServiceWorkerState::BrowserState::kReady; |
| |
| // Load the extension service worker. This method will wait for its |
| // registration to be stored and the service worker to be running. |
| ASSERT_NO_FATAL_FAILURE(LoadServiceWorkerExtension()); |
| // Load the sub-scope service worker and open the extension tab. |
| // This method will wait for the registration to be stored. |
| ASSERT_NO_FATAL_FAILURE(LoadSubScopeServiceWorker()); |
| |
| // Confirm that we are tracking the main extension service worker. |
| std::optional<WorkerId> extension_service_worker_id = |
| GetWorkerIdForExtension(); |
| ASSERT_TRUE(extension_service_worker_id); |
| |
| // Check that there's 2 service workers running in total. |
| content::ServiceWorkerContext* sw_context = |
| GetServiceWorkerContext(profile()); |
| ASSERT_TRUE(sw_context); |
| EXPECT_EQ(sw_context->GetRunningServiceWorkerInfos().size(), 2ul); |
| // One of them should be the main extension service worker. |
| EXPECT_TRUE(sw_context->GetRunningServiceWorkerInfos().contains( |
| extension_service_worker_id->version_id)); |
| |
| // Stop the sub-scope service worker. |
| TestServiceWorkerTaskQueueObserver untracked_observer; |
| GURL sub_scope(extension()->url().spec() + "subscope/"); |
| content::StopServiceWorkerForScope(sw_context, sub_scope, base::DoNothing()); |
| // Wait until the code responsible for untracking workers is called. |
| untracked_observer.WaitForUntrackServiceWorkerState(sub_scope); |
| |
| // Verify that the main extension service worker is still tracked as running |
| // by the task queue. |
| ServiceWorkerState* worker_state = GetWorkerState(); |
| EXPECT_EQ(worker_state->browser_state(), kExpectedBrowserState); |
| EXPECT_EQ(worker_state->renderer_state(), |
| ServiceWorkerState::RendererState::kActive); |
| EXPECT_TRUE(worker_state->worker_id()); |
| } |
| |
| // Toggle `extensions_features::OptimizeServiceWorkerStartRequests`. |
| INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE( |
| ServiceWorkerSubScopeWorkerTrackingBrowserTestWithOptimizeServiceWorkerStart); |
| |
| } // namespace |
| |
| } // namespace extensions |