blob: e4ae90ce5f3b1290f757224a468283902d9fc3ba [file] [log] [blame]
// 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 "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.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/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/browser/service_worker/service_worker_task_queue.h"
#include "extensions/browser/service_worker/worker_id_set.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.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "url/gurl.h"
// Tests for extension service worker behavior outside of worker API or event
// dispatching logic.
namespace extensions {
namespace {
// A helper class that intercepts the
// `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom receiver method,
// provides some of the call's arguments to an optional observer, 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`
// which can be destroyed due to the worker stop request sent during 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 when 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.
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` implemenation.";
return worker_host;
}
using DidStopServiceWorkerContextObserver =
base::RepeatingCallback<void(const ExtensionId& extension_id,
int64_t service_worker_version_id)>;
void SetDidStopServiceWorkerContextObserver(
DidStopServiceWorkerContextObserver did_stop_worker_observer) {
did_stop_worker_observer_ = std::move(did_stop_worker_observer);
}
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 {
if (did_stop_worker_observer_) {
did_stop_worker_observer_.Run(extension_id, service_worker_version_id);
}
// Do not call `real_service_worker_host_service_`'s
// `ServiceWorkerHost::DidStopServiceWorkerContext()` method to simulate
// that a stop notification was never sent from the renderer worker thread.
}
private:
DidStopServiceWorkerContextObserver did_stop_worker_observer_;
const WorkerId worker_id_;
};
// A helper class to wait for a service worker for an extension with
// `extension_id` to be initialized (and indirectly know that the new worker
// should've been added to `WorkerIdSet`).
class WorkerInitWaiter : public ServiceWorkerTaskQueue::TestObserver {
public:
explicit WorkerInitWaiter(const ExtensionId& extension_id)
: extension_id_(extension_id) {
ServiceWorkerTaskQueue::SetObserverForTest(this);
}
~WorkerInitWaiter() override {
ServiceWorkerTaskQueue::SetObserverForTest(nullptr);
}
void WaitForInit() { worker_inited_run_loop.Run(); }
private:
// ServiceWorkerTaskQueue::TestObserver:
void DidInitializeServiceWorkerContext(
const ExtensionId& extension_id) override {
if (extension_id == extension_id_) {
worker_inited_run_loop.Quit();
}
}
const std::string extension_id_;
base::RunLoop worker_inited_run_loop;
};
class ServiceWorkerTrackingBrowserTest : public ExtensionBrowserTest {
public:
ServiceWorkerTrackingBrowserTest()
// Prevent the test from hitting CHECKs so we can examine `WorkerIdSet` at
// the end of the test.
: allow_multiple_worker_per_extension_in_worker_id_set_(
WorkerIdSet::AllowMultipleWorkersPerExtensionForTesting()),
allow_multiple_workers_per_extension_in_task_queue_(
ServiceWorkerTaskQueue::
AllowMultipleWorkersPerExtensionForTesting()) {}
ServiceWorkerTrackingBrowserTest(const ServiceWorkerTrackingBrowserTest&) =
delete;
ServiceWorkerTrackingBrowserTest& operator=(
const ServiceWorkerTrackingBrowserTest&) = delete;
protected:
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
// Navigates the browser to a new tab at `url` and waits for it to load.
void NavigateInNewTab(const GURL& url) {
ui_test_utils::NavigateToURLWithDisposition(
browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::WaitForLoadStop(web_contents);
}
private:
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 this test by using similar logic to
// ServiceWorkerVersionTest.StallInStopping_DetachThenStart to more closely
// simulate a worker thread delayed in stopping.
// 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)
//
// the //extensions browser layer should only track one worker instance (the new
// worker instance). This avoids tracking multiple shutdown instances of the
// worker. Regression test for crbug.com/40936639.
IN_PROC_BROWSER_TEST_F(ServiceWorkerTrackingBrowserTest,
WorkerStalledInStopping) {
// 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"
}
})";
// The extensions script listens for runtime.onInstalled.
static constexpr char kBackgroundScript[] =
R"({
chrome.runtime.onInstalled.addListener((details) => {
// Asynchronously send the message that the listener fired so that the
// event is considered ack'd in the browser C++ code.
setTimeout(() => {
chrome.test.sendMessage('installed listener fired');
}, 0);
});
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundScript);
test_dir.WriteFile(FILE_PATH_LITERAL("extension_page_tab.html"),
"<p>page</p>");
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
// First SW version ID is always 0 and remains consistent until the extension
// is deactivated.
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(
GetServiceWorkerContext(), /*service_worker_version_id=*/0));
// 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`).
const GURL extension_tab_url = GURL(base::StringPrintf(
"%s/extension_page_tab.html", extension->url().spec().c_str()));
NavigateInNewTab(extension_tab_url);
// Setup intercept of `ServiceWorkerHost::DidStopServiceWorkerContext()` mojom
// call. This simulates the worker thread being very slow/never informing the
// //extensions browser layer that the worker context/thread terminated.
std::vector<WorkerId> service_workers_for_extension =
ProcessManager::Get(browser()->profile())
->GetServiceWorkersForExtension(extension->id());
ASSERT_EQ(service_workers_for_extension.size(), 1u);
ServiceWorkerHostInterceptorForWorkerStop stop_interceptor(
service_workers_for_extension[0]);
stop_interceptor.SetDidStopServiceWorkerContextObserver(
base::BindLambdaForTesting([&](const ExtensionId& extension_id,
int64_t service_worker_version_id) {
ASSERT_EQ(extension->id(), extension_id);
ASSERT_EQ(service_worker_version_id, 0);
}));
// 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(
browser()->profile(), extension->id());
ASSERT_TRUE(content::CheckServiceWorkerIsStopped(
GetServiceWorkerContext(), /*service_worker_version_id=*/0));
// Confirm after stopping we no longer have the previous `WorkerId`.
ProcessManager* process_manager = ProcessManager::Get(profile());
ASSERT_TRUE(process_manager);
std::vector<WorkerId> service_workers_after_stop_worker =
process_manager->GetServiceWorkersForExtension(extension->id());
// TODO(crbug.com/40936639): Once this bug is fixed, enable this assert.
// ASSERT_TRUE(service_workers_after_stop_worker.empty());
ASSERT_EQ(service_workers_after_stop_worker.size(), 1u);
// Add an observer to the task queue to detect when the new worker instance
// `WorkerId` is added to `WorkerIdSet`.
WorkerInitWaiter worker_id_added_observer(extension->id());
// Start the new instance of the worker and wait for it to start.
int new_worker_thread_id = -1;
base::RunLoop start_worker_loop;
ASSERT_TRUE(GetServiceWorkerContext());
GetServiceWorkerContext()->StartWorkerForScope(
/*scope=*/extension->url(),
/*key=*/
blink::StorageKey::CreateFirstParty(
url::Origin::Create(extension->url())),
/*info_callback=*/
base::BindLambdaForTesting([&new_worker_thread_id, &start_worker_loop](
int64_t version_id, int process_id,
int started_worker_thread_id) {
new_worker_thread_id = started_worker_thread_id;
start_worker_loop.Quit();
}),
/*failure_callback=*/
base::BindLambdaForTesting(
[](blink::ServiceWorkerStatusCode status_code) {
GTEST_FAIL() << "Starting the new worker instance failed.";
}));
start_worker_loop.Run();
// Wait for the new worker instance to be added to `WorkerIdSet`.
worker_id_added_observer.WaitForInit();
std::vector<WorkerId> service_workers_after_restarted_worker =
process_manager->GetServiceWorkersForExtension(extension->id());
// TODO(crbug.com/40936639): Once this bug is fixed, enable this expect.
// EXPECT_EQ(service_workers_after_restarted_worker.size(), 1u);
EXPECT_EQ(service_workers_after_restarted_worker.size(), 2u);
// Confirm `WorkerId` being tracked is the same newly started instance above.
EXPECT_EQ(service_workers_after_restarted_worker.back().thread_id,
new_worker_thread_id);
}
} // namespace
} // namespace extensions