blob: 4a1d062c14f0a09a16a88411686ddd4b57fb1f6f [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stddef.h>
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/extensions/api/permissions/permissions_api.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_management_test_util.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/test/test_browser_closed_waiter.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/profile_destruction_waiter.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/api/test/test_api.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/service_worker/service_worker_keepalive.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/common/extension.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "url/gurl.h"
namespace extensions {
namespace {
constexpr char kTestOpenerExtensionId[] = "adpghjkjicpfhcjicmiifjpbalaildpo";
constexpr char kTestOpenerExtensionUrl[] =
"chrome-extension://adpghjkjicpfhcjicmiifjpbalaildpo/";
constexpr char kTestOpenerExtensionRelativePath[] =
"service_worker/policy/opener_extension";
constexpr char kTestReceiverExtensionId[] = "eagjmgdicfmccfhiiihnaehbfheheidk";
constexpr char kTestReceiverExtensionUrl[] =
"chrome-extension://eagjmgdicfmccfhiiihnaehbfheheidk/";
constexpr char kTestReceiverExtensionRelativePath[] =
"service_worker/policy/receiver_extension";
constexpr char kPersistentPortConnectedMessage[] = "Persistent port connected";
constexpr char kPersistentPortDisconnectedMessage[] =
"Persistent port disconnected";
// Gets a keepalive matcher that enforces the extra data field.
testing::Matcher<ProcessManager::ServiceWorkerKeepaliveData>
GetKeepaliveMatcher(const WorkerId& worker_id,
Activity::Type type,
const std::string& activity_extra_data) {
return testing::AllOf(
testing::Field("worker_id",
&ProcessManager::ServiceWorkerKeepaliveData::worker_id,
worker_id),
testing::Field("activity_type",
&ProcessManager::ServiceWorkerKeepaliveData::activity_type,
type),
testing::Field("extra_data",
&ProcessManager::ServiceWorkerKeepaliveData::extra_data,
activity_extra_data));
}
// Gets a keepalive matcher enforcing only the worker ID and activity type.
testing::Matcher<ProcessManager::ServiceWorkerKeepaliveData>
GetKeepaliveMatcher(const WorkerId& worker_id, Activity::Type type) {
return testing::AllOf(
testing::Field("worker_id",
&ProcessManager::ServiceWorkerKeepaliveData::worker_id,
worker_id),
testing::Field("activity_type",
&ProcessManager::ServiceWorkerKeepaliveData::activity_type,
type));
}
// Returns the number of active external requests to the service worker of
// the specified `extension` in the given `context`.
size_t GetExternalRequestCountForWorker(content::BrowserContext& context,
const Extension& extension) {
const blink::StorageKey extension_key =
blink::StorageKey::CreateFirstParty(extension.origin());
return service_worker_test_utils::GetServiceWorkerContext(&context)
->CountExternalRequestsForTest(extension_key);
}
} // namespace
using service_worker_test_utils::TestServiceWorkerContextObserver;
class ServiceWorkerLifetimeKeepaliveBrowsertest : public ExtensionApiTest {
public:
ServiceWorkerLifetimeKeepaliveBrowsertest() {
// TODO(crbug.com/40937027): Convert test to use HTTPS and then re-enable.
feature_list_.InitAndDisableFeature(features::kHttpsFirstModeIncognito);
}
ServiceWorkerLifetimeKeepaliveBrowsertest(
const ServiceWorkerLifetimeKeepaliveBrowsertest&) = delete;
ServiceWorkerLifetimeKeepaliveBrowsertest& operator=(
const ServiceWorkerLifetimeKeepaliveBrowsertest&) = delete;
~ServiceWorkerLifetimeKeepaliveBrowsertest() override = default;
void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(StartEmbeddedTestServer());
}
void TearDownOnMainThread() override {
ExtensionApiTest::TearDownOnMainThread();
// Some tests use SetTickClockForTesting() with `tick_clock_opener_` or
// `tick_clock_receiver_`. Restore the TickClock to the default now.
// This is required because the TickClock must outlive ServiceWorkerVersion,
// otherwise ServiceWorkerVersion will hold a dangling pointer.
content::ResetTickClockToDefaultForAllLiveServiceWorkerVersions(
GetServiceWorkerContext());
}
void TriggerTimeoutAndCheckActive(content::ServiceWorkerContext* context,
int64_t version_id) {
EXPECT_TRUE(
content::TriggerTimeoutAndCheckRunningState(context, version_id));
}
void TriggerTimeoutAndCheckStopped(content::ServiceWorkerContext* context,
int64_t version_id) {
EXPECT_FALSE(
content::TriggerTimeoutAndCheckRunningState(context, version_id));
}
base::SimpleTestTickClock tick_clock_opener_;
base::SimpleTestTickClock tick_clock_receiver_;
private:
base::test::ScopedFeatureList feature_list_;
};
// Loads two extensions that open a persistent port connection between each
// other and tests that their service worker will stop after kRequestTimeout (5
// minutes).
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ServiceWorkersTimeOutWithoutPolicy) {
content::ServiceWorkerContext* context = GetServiceWorkerContext();
TestServiceWorkerContextObserver sw_observer_receiver_extension(
context, kTestReceiverExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
const int64_t service_worker_receiver_id =
sw_observer_receiver_extension.WaitForWorkerStarted();
ExtensionTestMessageListener connect_listener(
kPersistentPortConnectedMessage);
connect_listener.set_extension_id(kTestReceiverExtensionId);
TestServiceWorkerContextObserver sw_observer_opener_extension(
context, kTestOpenerExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
const int64_t service_worker_opener_id =
sw_observer_opener_extension.WaitForWorkerStarted();
ASSERT_TRUE(connect_listener.WaitUntilSatisfied());
// Advance clock and check that the receiver service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
&tick_clock_receiver_);
TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);
sw_observer_receiver_extension.WaitForWorkerStopped();
// Advance clock and check that the opener service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
sw_observer_opener_extension.WaitForWorkerStopped();
}
// Tests that the service workers will not stop if both extensions are
// allowlisted via policy and the port is not closed.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ServiceWorkersDoNotTimeOutWithPolicy) {
base::Value::List urls;
// Both extensions receive extended lifetime.
urls.Append(kTestOpenerExtensionUrl);
urls.Append(kTestReceiverExtensionUrl);
profile()->GetPrefs()->SetList(
pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
std::move(urls));
content::ServiceWorkerContext* context = GetServiceWorkerContext();
// Load the extensions and wait for the service workers to be activated. This
// test advances the worker's clock. If the activation request is in-flight
// when the clock is advanced, the request will expire and the worker will be
// terminated (because activation requests have KILL_ON_TIMEOUT behavior).
// Thus, we ensure that there are no in-flight activation requests before
// advancing the clock.
TestServiceWorkerContextObserver sw_observer_receiver_extension(
context, kTestReceiverExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
const int64_t service_worker_receiver_id =
sw_observer_receiver_extension.WaitForWorkerActivated();
ExtensionTestMessageListener connect_listener(
kPersistentPortConnectedMessage);
connect_listener.set_extension_id(kTestReceiverExtensionId);
TestServiceWorkerContextObserver sw_observer_opener_extension(
context, kTestOpenerExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
const int64_t service_worker_opener_id =
sw_observer_opener_extension.WaitForWorkerActivated();
ASSERT_TRUE(connect_listener.WaitUntilSatisfied());
// Advance clock and check that the receiver service worker did not stop.
content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
&tick_clock_receiver_);
TriggerTimeoutAndCheckActive(context, service_worker_receiver_id);
// Advance clock and check that the opener service worker did not stop.
content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckActive(context, service_worker_opener_id);
}
// Tests that the extended lifetime only lasts as long as there is a persistent
// port connection. If the port is closed (by one of the service workers
// stopping), the other service worker will also stop, even if it received an
// extended lifetime.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ServiceWorkersTimeOutWhenOnlyOneHasExtendedLifetime) {
base::Value::List urls;
// Opener extension will receive extended lifetime because it connects to a
// policy allowlisted extension.
urls.Append(kTestReceiverExtensionUrl);
profile()->GetPrefs()->SetList(
pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
std::move(urls));
content::ServiceWorkerContext* context = GetServiceWorkerContext();
TestServiceWorkerContextObserver sw_observer_receiver_extension(
context, kTestReceiverExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
const int64_t service_worker_receiver_id =
sw_observer_receiver_extension.WaitForWorkerStarted();
ExtensionTestMessageListener connect_listener(
kPersistentPortConnectedMessage);
connect_listener.set_extension_id(kTestReceiverExtensionId);
TestServiceWorkerContextObserver sw_observer_opener_extension(
context, kTestOpenerExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
const int64_t service_worker_opener_id =
sw_observer_opener_extension.WaitForWorkerStarted();
ASSERT_TRUE(connect_listener.WaitUntilSatisfied());
ExtensionTestMessageListener disconnect_listener(
kPersistentPortDisconnectedMessage);
disconnect_listener.set_extension_id(kTestOpenerExtensionId);
// Advance clock and check that the receiver service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
&tick_clock_receiver_);
TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);
// Wait for the receiver SW to be closed in order for the port to be
// disconnected and the opener SW losing extended lifetime.
sw_observer_receiver_extension.WaitForWorkerStopped();
// Wait for port to close in the opener extension.
ASSERT_TRUE(disconnect_listener.WaitUntilSatisfied());
// Advance clock and check that the opener service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
sw_observer_opener_extension.WaitForWorkerStopped();
}
// Tests that the service workers will stop if both extensions are allowlisted
// via policy and the port is disconnected.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ServiceWorkersTimeOutWhenPortIsDisconnected) {
base::Value::List urls;
// Both extensions receive extended lifetime.
urls.Append(kTestReceiverExtensionUrl);
urls.Append(kTestOpenerExtensionUrl);
profile()->GetPrefs()->SetList(
pref_names::kExtendedBackgroundLifetimeForPortConnectionsToUrls,
std::move(urls));
content::ServiceWorkerContext* context = GetServiceWorkerContext();
TestServiceWorkerContextObserver sw_observer_receiver_extension(
context, kTestReceiverExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestReceiverExtensionRelativePath));
const int64_t service_worker_receiver_id =
sw_observer_receiver_extension.WaitForWorkerStarted();
ExtensionTestMessageListener connect_listener(
kPersistentPortConnectedMessage);
connect_listener.set_extension_id(kTestReceiverExtensionId);
TestServiceWorkerContextObserver sw_observer_opener_extension(
context, kTestOpenerExtensionId);
LoadExtension(test_data_dir_.AppendASCII(kTestOpenerExtensionRelativePath));
const int64_t service_worker_opener_id =
sw_observer_opener_extension.WaitForWorkerStarted();
ASSERT_TRUE(connect_listener.WaitUntilSatisfied());
ExtensionTestMessageListener disconnect_listener(
kPersistentPortDisconnectedMessage);
disconnect_listener.set_extension_id(kTestOpenerExtensionId);
// Disconnect the port from the receiver extension.
constexpr char kDisconnectScript[] = R"(port.disconnect();)";
BackgroundScriptExecutor script_executor(profile());
script_executor.ExecuteScriptAsync(
kTestReceiverExtensionId, kDisconnectScript,
BackgroundScriptExecutor::ResultCapture::kNone,
browsertest_util::ScriptUserActivation::kDontActivate);
// Wait for port to close in the opener extension.
ASSERT_TRUE(disconnect_listener.WaitUntilSatisfied());
// Advance clock and check that the receiver service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_receiver_id,
&tick_clock_receiver_);
TriggerTimeoutAndCheckStopped(context, service_worker_receiver_id);
// Wait for the receiver SW to be closed.
sw_observer_receiver_extension.WaitForWorkerStopped();
// Advance clock and check that the opener service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_opener_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, service_worker_opener_id);
sw_observer_opener_extension.WaitForWorkerStopped();
}
// Tests that certain API functions can keep the service worker alive
// indefinitely.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
KeepalivesForCertainExtensionFunctions) {
static constexpr char kManifest[] =
R"({
"name": "test extension",
"manifest_version": 3,
"background": {"service_worker": "background.js"},
"version": "0.1",
"optional_permissions": ["tabs"]
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// blank");
// Load up the extension and wait for the worker to start.
TestServiceWorkerContextObserver registration_observer(profile());
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
// We explicitly wait for the worker to be activated. Otherwise, the
// activation event might still be running when we advance the timer, causing
// the worker to be killed for the activation event timing out.
int64_t version_id = registration_observer.WaitForWorkerActivated();
// Inject a script that will trigger chrome.permissions.request() and then
// return. When permissions.request() resolves, it will send a message.
static constexpr char kTriggerPrompt[] =
R"(chrome.test.runWithUserGesture(() => {
chrome.permissions.request({permissions: ['tabs']}).then(() => {
chrome.test.sendMessage('resolved');
});
chrome.test.sendScriptResult('success');
});)";
// Programmatically control the permissions request result. This allows us
// to control when it is resolved.
auto dialog_action_reset =
PermissionsRequestFunction::SetDialogActionForTests(
PermissionsRequestFunction::DialogAction::kProgrammatic);
base::Value result = BackgroundScriptExecutor::ExecuteScript(
profile(), extension->id(), kTriggerPrompt,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("success", result);
content::ServiceWorkerContext* context = GetServiceWorkerContext();
// Right now, the permissions request should be pending. Since
// `permissions.request()` is specified as a function that can keep the
// extension worker alive indefinitely, advancing the clock and triggering the
// timeout should not result in a worker kill.
content::AdvanceClockAfterRequestTimeout(context, version_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckActive(context, version_id);
{
ExtensionTestMessageListener listener("resolved");
// Resolve the pending dialog and wait for the resulting message.
PermissionsRequestFunction::ResolvePendingDialogForTests(false);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// We also run a run loop here so that the keepalive from the
// test.sendMessage() call is resolved.
base::RunLoop().RunUntilIdle();
}
// Advance the timer again. This should result in the worker being stopped,
// since the permissions.request() function call is now completed.
content::AdvanceClockAfterRequestTimeout(context, version_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, version_id);
}
// Test the flow of an extension function resolving after an extension service
// worker has timed out and been terminated.
// Regression test for https://crbug.com/1453534.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ExtensionFunctionGetsResolvedAfterWorkerTermination) {
static constexpr char kManifest[] =
R"({
"name": "test extension",
"manifest_version": 3,
"background": {"service_worker": "background.js"},
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "// blank");
// Load up the extension and wait for the worker to start.
TestServiceWorkerContextObserver registration_observer(profile());
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
// We explicitly wait for the worker to be activated. Otherwise, the
// activation event might still be running when we advance the timer, causing
// the worker to be killed for the activation event timing out.
int64_t version_id = registration_observer.WaitForWorkerActivated();
// Inject a trivial script that will call test.sendMessage(). This is a handy
// API because, by indicating the test will reply, we control when the
// function is resolved.
static constexpr char kScript[] =
"chrome.test.sendMessage('hello', () => {});";
ExtensionTestMessageListener message_listener("hello",
ReplyBehavior::kWillReply);
BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension->id(),
kScript);
ASSERT_TRUE(message_listener.WaitUntilSatisfied());
content::ServiceWorkerContext* context = GetServiceWorkerContext();
TestServiceWorkerContextObserver context_observer(context, extension->id());
context_observer.SetRunningId(version_id);
// Advance the request past the timeout. Since test.sendMessage() doesn't
// keep a worker alive indefinitely, the service worker should be terminated.
content::AdvanceClockAfterRequestTimeout(context, version_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, version_id);
// Wait for the worker to fully stop.
context_observer.WaitForWorkerStopped();
// Reply to the extension (even though the worker is gone). This triggers
// the completion of the extension function, which would otherwise try to
// decrement the keepalive count of the worker. The worker was already
// terminated; it should gracefully handle this case (as opposed to crash).
message_listener.Reply("foo");
}
// Tests that an active debugger session will keep an extension service worker
// alive past its typical timeout.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
DebuggerAttachKeepsServiceWorkerAlive) {
static constexpr char kManifest[] =
R"({
"name": "Debugger attach",
"manifest_version": 3,
"version": "0.1",
"permissions": ["debugger"],
"background": {
"service_worker": "background.js"
}
})";
// A simple background script that knows how to attach and detach a debugging
// session from a target (active) tab.
static constexpr char kBackgroundJs[] =
R"(let attachedTab;
async function attachToActiveTab() {
let tabs =
await chrome.tabs.query({active: true, currentWindow: true});
let tab = tabs[0];
await chrome.debugger.attach({tabId: tab.id}, '1.3');
attachedTab = tab;
chrome.test.sendScriptResult('attached');
}
async function detach() {
await chrome.debugger.detach({tabId: attachedTab.id});
chrome.test.sendScriptResult('detached');
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
// Load up the extension and wait for the worker to start.
TestServiceWorkerContextObserver registration_observer(profile());
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
// We explicitly wait for the worker to be activated. Otherwise, the
// activation event might still be running when we advance the timer, causing
// the worker to be killed for the activation event timing out.
int64_t version_id = registration_observer.WaitForWorkerActivated();
// Open a new tab for the extension to attach a debugger to.
const GURL example_com =
embedded_test_server()->GetURL("example.com", "/simple.html");
auto* web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(web_contents, example_com));
EXPECT_EQ(example_com, web_contents->GetLastCommittedURL());
// Attach the extension debugger.
EXPECT_EQ("attached",
BackgroundScriptExecutor::ExecuteScript(
profile(), extension->id(), "attachToActiveTab();",
BackgroundScriptExecutor::ResultCapture::kSendScriptResult));
// Ensure the keepalive associated with sendScriptResult() has resolved.
base::RunLoop().RunUntilIdle();
content::ServiceWorkerContext* context = GetServiceWorkerContext();
// Since the extension has an active debugger session, it should not be
// terminated, even for going past the typical time limit.
content::AdvanceClockAfterRequestTimeout(context, version_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckActive(context, version_id);
// Have the extension detach its debugging session.
EXPECT_EQ("detached",
BackgroundScriptExecutor::ExecuteScript(
profile(), extension->id(), "detach();",
BackgroundScriptExecutor::ResultCapture::kSendScriptResult));
// Ensure the keepalive associated with sendScriptResult() has resolved.
base::RunLoop().RunUntilIdle();
// The extension service worker should now be terminated, since it no longer
// has an active debug session.
content::AdvanceClockAfterRequestTimeout(context, version_id,
&tick_clock_opener_);
TriggerTimeoutAndCheckStopped(context, version_id);
}
// Tests the behavior of the ServiceWorkerKeepalive struct, ensuring it properly
// keeps the service worker alive.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ServiceWorkerKeepaliveUtility) {
// Load up a simple extension and grab its service worker data.
static constexpr char kManifest[] =
R"({
"name": "Test",
"version": "0.1",
"manifest_version": 3,
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackground[] = R"(chrome.test.sendMessage('ready');)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
ExtensionTestMessageListener ready_listener("ready");
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
ASSERT_TRUE(extension);
ASSERT_TRUE(ready_listener.WaitUntilSatisfied());
// Note: We RunUntilIdle() to ensure the implementation handling of the
// test.sendMessage() API call has finished; otherwise, that affects our
// keepalives.
base::RunLoop().RunUntilIdle();
ProcessManager* process_manager = ProcessManager::Get(profile());
std::vector<WorkerId> worker_ids =
process_manager->GetServiceWorkersForExtension(extension->id());
ASSERT_EQ(1u, worker_ids.size());
WorkerId worker_id = worker_ids[0];
// To begin, there should be no associated keepalives for the extension.
EXPECT_EQ(0u, process_manager
->GetServiceWorkerKeepaliveDataForRecords(extension->id())
.size());
// Create a single keepalive.
std::optional<ServiceWorkerKeepalive> function_keepalive(
ServiceWorkerKeepalive(
profile(), worker_id,
content::ServiceWorkerExternalRequestTimeoutType::kDefault,
Activity::API_FUNCTION, "alarms.create"));
// There should be one keepalive for the extension.
EXPECT_THAT(
process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
testing::UnorderedElementsAre(GetKeepaliveMatcher(
worker_id, Activity::API_FUNCTION, "alarms.create")));
// Create a second keepalive (an event-related one).
std::optional<ServiceWorkerKeepalive> event_keepalive(ServiceWorkerKeepalive(
profile(), worker_id,
content::ServiceWorkerExternalRequestTimeoutType::kDefault,
Activity::EVENT, "alarms.onAlarm"));
// Now, there should be two keepalives.
EXPECT_THAT(
process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
testing::UnorderedElementsAre(
GetKeepaliveMatcher(worker_id, Activity::API_FUNCTION,
"alarms.create"),
GetKeepaliveMatcher(worker_id, Activity::EVENT, "alarms.onAlarm")));
// Reset the first. There should now be only the second keepalive.
function_keepalive.reset();
EXPECT_THAT(
process_manager->GetServiceWorkerKeepaliveDataForRecords(extension->id()),
testing::UnorderedElementsAre(
GetKeepaliveMatcher(worker_id, Activity::EVENT, "alarms.onAlarm")));
// Reset the second, and the keepalive count should go to zero.
event_keepalive.reset();
EXPECT_EQ(0u, process_manager
->GetServiceWorkerKeepaliveDataForRecords(extension->id())
.size());
}
// Tests shutting down the associated browser context while the extension has
// an active keepalive from a message pipe behaves appropriately.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ShutdownWithActiveMessagePipe) {
// Load an extension with incognito split mode and a content script that
// runs on example.com.
// The split mode incognito is important so that we can fully shut down a
// browser context with separately-tracked keepalives.
static constexpr char kManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"incognito": "split",
"background": {"service_worker": "background.js"},
"content_scripts": [
{
"js": ["content_script.js"],
"matches": ["*://example.com/*"],
"run_at": "document_end"
}
]
})";
static constexpr char kBackgroundJs[] = R"(// Intentionally blank.)";
// The content script adds a listener for a new message and then
// (asynchronously) signals success.
// See keepalive comments below for why this is async.
// NOTE: We're careful not to have the port be garbage collected by storing
// it on `self`; otherwise this could close the message pipe.
static constexpr char kContentScriptJs[] =
R"(chrome.runtime.onMessage.addListener((msg, sender, reply) => {
self.reply = reply;
setTimeout(() => { chrome.test.sendScriptResult('success'); }, 0);
// Indicates async response, keeping the message pipe open.
return true;
});
chrome.test.sendMessage('content script ready');)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
test_dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScriptJs);
const Extension* extension =
LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(extension);
Profile* incognito_profile =
profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
TestServiceWorkerContextObserver registration_observer(incognito_profile);
// Open example.com/simple.html in an incognito window. The content script
// will inject.
ExtensionTestMessageListener content_script_listener("content script ready");
Browser* incognito_browser = OpenURLOffTheRecord(
profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
ASSERT_TRUE(content_script_listener.WaitUntilSatisfied());
registration_observer.WaitForWorkerActivated();
content::WebContents* incognito_tab =
incognito_browser->tab_strip_model()->GetActiveWebContents();
int tab_id = ExtensionTabUtil::GetTabId(incognito_tab);
// Send a message to the incognito tab from the incognito service worker.
// This will open a message pipe. Since the content script never responds,
// the message pipe will remain open.
static constexpr char kOpenMessagePipe[] =
R"((async () => {
// Note: Pass a callback to signal a reply is expected.
chrome.tabs.sendMessage(%d, 'hello', () => {});
})();)";
base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
incognito_profile, extension->id(),
base::StringPrintf(kOpenMessagePipe, tab_id),
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("success", script_result);
ProcessManager* incognito_process_manager =
ProcessManager::Get(incognito_profile);
// Grab the active worker for the incognito context.
std::vector<WorkerId> worker_ids =
incognito_process_manager->GetServiceWorkersForExtension(extension->id());
ASSERT_EQ(1u, worker_ids.size());
WorkerId worker_id = worker_ids[0];
// Verify the service worker currently has a keepalive for the message
// port.
// The keepalive flow is as follows:
// * Service worker opens a message pipe. New Activity::MESSAGE_PORT
// keepalive from the worker context.
// * Message pipe is opened in the tab. New Activity::MESSAGE_PORT
// keepalive from the tab context.
// * Message is sent to the tab. New Activity::MESSAGE keepalive from
// the tab context.
// * The message is ack'd from the tab. Activity::MESSAGE keepalive
// from the tab context is removed. Since we signal success in the
// tab asynchronously, the keepalive is guaranteed to have resolved.
// (Otherwise, it could potentially be racy).
// Thus, at the end, we have two remaining keepalives.
// TODO(crbug.com/41487026): Ideally, there would only be one -- we shouldn't
// add keepalives for the service worker due to a tab's message port.
EXPECT_THAT(
incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
extension->id()),
testing::UnorderedElementsAre(
GetKeepaliveMatcher(worker_id, Activity::MESSAGE_PORT),
GetKeepaliveMatcher(worker_id, Activity::MESSAGE_PORT)));
// Close the incognito browser while the message channel is still open. Since
// this is the only browser window for the incognito context, this also
// results in the browser context being invalidated.
ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
TestBrowserClosedWaiter browser_closed_waiter(incognito_browser);
incognito_browser->window()->Close();
ASSERT_TRUE(browser_closed_waiter.WaitUntilClosed());
profile_destruction_waiter.Wait();
// Note: `ProfileDestructionWaiter` only waits for the profile to signal it
// *will* be destroyed. Spin once to finish the job.
base::RunLoop().RunUntilIdle();
// Verify the profile is destroyed.
EXPECT_FALSE(
ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));
// The test succeeds if there are no crashes. There's nothing left to verify
// for keepalives, since the profile is gone.
}
// Tests that we can safely shut down a BrowserContext when an extension has
// an active message port to another extension, where each are running in
// split incognito mode.
// Regression test for https://crbug.com/1476316.
IN_PROC_BROWSER_TEST_F(ServiceWorkerLifetimeKeepaliveBrowsertest,
ShutdownWithActiveMessagePipe_SplitModeExtension) {
// A split-mode extension. This will have a separate process for the on- and
// off-the-record profiles.
static constexpr char kManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"incognito": "split",
"background": {"service_worker": "background.js"}
})";
// A background page that knows how to open a message pipe to another
// extension.
static constexpr char kOpenerBackgroundJs[] =
R"(async function openMessagePipe(listenerId) {
// Note: Pass a callback to signal a reply is expected.
chrome.runtime.sendMessage(listenerId, 'hello', () => {});
})";
// The listener extension will listen for an external message (from the
// opener mode extension). We save the `sendReply` callback so it's not
// garbage collected and keeps the message pipe open, and then asynchronously
// respond that the message was received. The asynchronous response is
// important in order to ensure the message being received from this
// extension is properly ack'd.
static constexpr char kListenerBackgroundJs[] =
R"(chrome.runtime.onMessageExternal.addListener(
(msg, sender, sendReply) => {
self.sendReply = sendReply;
setTimeout(() => { chrome.test.sendScriptResult('success'); });
return true;
});)";
TestExtensionDir opener_extension_dir;
opener_extension_dir.WriteManifest(kManifest);
opener_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
kOpenerBackgroundJs);
TestExtensionDir listener_extension_dir;
listener_extension_dir.WriteManifest(kManifest);
listener_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
kListenerBackgroundJs);
const Extension* opener_extension = LoadExtension(
opener_extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(opener_extension);
const Extension* listener_extension = LoadExtension(
listener_extension_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(listener_extension);
Profile* incognito_profile =
profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
// TODO(crbug.com/335829868): Refactor to use
// ServiceWorkerTaskQueue::TestObserver::DidStartWorker() to ensure worker is
// ready to receive event in BackgroundScriptExecutor::ExecuteScript().
TestServiceWorkerContextObserver sw_observer_opener_extension(
incognito_profile, opener_extension->id());
TestServiceWorkerContextObserver sw_observer_listener_extension(
incognito_profile, listener_extension->id());
// Open a new tab in incognito. This spawns the new process for the split mode
// extensions.
Browser* incognito_browser = OpenURLOffTheRecord(
profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
sw_observer_listener_extension.WaitForWorkerStarted();
sw_observer_opener_extension.WaitForWorkerStarted();
// Send a message from one extension to the other, opening a message pipe.
// Since the listener extension never responds, the message pipe will
// remain open. The listener then sends the script result 'success' when it
// receives the message.
static constexpr char kOpenMessagePipe[] = R"(openMessagePipe('%s');)";
base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
incognito_profile, opener_extension->id(),
base::StringPrintf(kOpenMessagePipe, listener_extension->id().c_str()),
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("success", script_result);
ProcessManager* incognito_process_manager =
ProcessManager::Get(incognito_profile);
// Grab each extension's active worker.
std::vector<WorkerId> opener_worker_ids =
incognito_process_manager->GetServiceWorkersForExtension(
opener_extension->id());
ASSERT_EQ(1u, opener_worker_ids.size());
WorkerId opener_worker_id = opener_worker_ids[0];
std::vector<WorkerId> listener_worker_ids =
incognito_process_manager->GetServiceWorkersForExtension(
listener_extension->id());
ASSERT_EQ(1u, listener_worker_ids.size());
WorkerId listener_worker_id = listener_worker_ids[0];
// Verify the service workers currently have a keepalive for the message
// port.
// The keepalive flow is as follows:
// * Open a new message port. Add keepalives for both extensions with
// Activity::MESSAGE_PORT.
// * Message is sent to the listener extension. New Activity::MESSAGE
// keepalive is added for the sender extension.
// * The message is ack'd from the listener extension's process.
// Activity::MESSAGE keepalive is removed for the sender extension.
// Since we signal success in the listener asynchronously, the keepalive is
// guaranteed to have resolved. (Otherwise, it could potentially be racy).
// * Send chrome.test.sendScriptResult() from the listener extension.
// Add and remove Activity::API_FUNCTION keepalives.
// Thus, at the end, the remaining keepalives are one MESSAGE_PORT keepalive
// for each extension.
EXPECT_THAT(
incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
opener_extension->id()),
testing::UnorderedElementsAre(
GetKeepaliveMatcher(opener_worker_id, Activity::MESSAGE_PORT)));
EXPECT_THAT(
incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
listener_extension->id()),
testing::UnorderedElementsAre(
GetKeepaliveMatcher(listener_worker_id, Activity::MESSAGE_PORT)));
// Close the incognito browser while the message channel is still open. Since
// this is the only browser window for the incognito context, this also
// results in the browser context being invalidated.
// As part of this, the keepalives are removed for the extensions, which
// can trigger an attempted removal of an external request from the
// service worker layer. Since the context is being shut down, this can
// fail with `content::ServiceWorkerExternalRequestResult::kNullContext`. This
// is fine, since the whole context is going away.
// See https://crbug.com/1476316.
ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
TestBrowserClosedWaiter browser_closed_waiter(incognito_browser);
incognito_browser->window()->Close();
ASSERT_TRUE(browser_closed_waiter.WaitUntilClosed());
profile_destruction_waiter.Wait();
// Note: `ProfileDestructionWaiter` only waits for the profile to signal it
// *will* be destroyed. Spin once to finish the job.
base::RunLoop().RunUntilIdle();
// Verify the profile is destroyed.
EXPECT_FALSE(
ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));
// The test succeeds if there are no crashes. There's nothing left to verify
// for keepalives, since the profile is gone.
}
// Tests that we can safely shut down a BrowserContext when an extension has an
// active keepalive for a service worker that spans between the on- and off-
// the-record profiles between two different extensions.
IN_PROC_BROWSER_TEST_F(
ServiceWorkerLifetimeKeepaliveBrowsertest,
ShutdownWithActiveMessagePipe_BetweenExtensionsInDifferentContexts) {
// A split-mode extension. This will have a separate process for the on- and
// off-the-record profiles.
static constexpr char kSplitModeManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"incognito": "split",
"background": {"service_worker": "background.js"}
})";
static constexpr char kSplitBackgroundJs[] = R"(// Intentionally blank.)";
// A spanning mode extension. This will share a process between the on- and
// off-the-record profiles.
static constexpr char kSpanningManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"incognito": "spanning",
"background": {"service_worker": "background.js"}
})";
// The spanning mode extension listens for an external message (from the
// split mode extension). We save the `sendReply` callback so it's not garbage
// collected and keeps the message pipe open, and then asynchronously respond
// that the message was received.
// The asynchronous response is important in order to ensure the message being
// received from this extension is properly ack'd in the renderer before the
// script result is received.
static constexpr char kSpanningModeBackgroundJs[] =
R"(chrome.runtime.onMessageExternal.addListener(
(msg, sender, sendReply) => {
self.sendReply = sendReply;
setTimeout(
() => { chrome.test.sendScriptResult('success'); }, 0);
return true;
});)";
TestExtensionDir split_mode_dir;
split_mode_dir.WriteManifest(kSplitModeManifest);
split_mode_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
kSplitBackgroundJs);
TestExtensionDir spanning_mode_dir;
spanning_mode_dir.WriteManifest(kSpanningManifest);
spanning_mode_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
kSpanningModeBackgroundJs);
const Extension* split_mode_extension = LoadExtension(
split_mode_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(split_mode_extension);
const Extension* spanning_mode_extension = LoadExtension(
spanning_mode_dir.UnpackedPath(), {.allow_in_incognito = true});
ASSERT_TRUE(spanning_mode_extension);
Profile* incognito_profile =
profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
// Wait for the single worker from split_mode_extension.
// TODO(crbug.com/335829868): Refactor to use
// ServiceWorkerTaskQueue::TestObserver::DidStartWorker() to ensure worker is
// ready to receive event in BackgroundScriptExecutor::ExecuteScript().
TestServiceWorkerContextObserver sw_observer(incognito_profile);
// Open a new tab in incognito. This spawns the new process for the split mode
// extension.
Browser* incognito_browser = OpenURLOffTheRecord(
profile(), embedded_test_server()->GetURL("example.com", "/simple.html"));
sw_observer.WaitForWorkerStarted();
// Send a message to the spanning mode extension from the incognito context of
// the split mode extension.
// This will open a message pipe. Since the spanning mode extension never
// responds, the message pipe will remain open.
// The spanning mode extension will then send the script result "success" when
// it receives the message.
static constexpr char kOpenMessagePipe[] =
R"((async () => {
// Note: Pass a callback to signal a reply is expected.
chrome.runtime.sendMessage('%s', 'hello', () => {});
})();)";
base::Value script_result = BackgroundScriptExecutor::ExecuteScript(
incognito_profile, split_mode_extension->id(),
base::StringPrintf(kOpenMessagePipe,
spanning_mode_extension->id().c_str()),
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
EXPECT_EQ("success", script_result);
ProcessManager* process_manager = ProcessManager::Get(profile());
ProcessManager* incognito_process_manager =
ProcessManager::Get(incognito_profile);
std::vector<WorkerId> worker_ids =
process_manager->GetServiceWorkersForExtension(
spanning_mode_extension->id());
ASSERT_EQ(1u, worker_ids.size());
WorkerId spanning_worker_id = worker_ids[0];
worker_ids = incognito_process_manager->GetServiceWorkersForExtension(
split_mode_extension->id());
ASSERT_EQ(1u, worker_ids.size());
WorkerId split_incognito_worker_id = worker_ids[0];
// Verify the current keepalives for the extensions.
// Each extension should have exactly one keepalive (the active message
// port). However, the context in which the keepalive is present is
// different:
// - The spanning mode extension should have a keepalive in the on-the-record
// context, and not in the incognito context (where it's not running).
// - The split mode extension should have a keepalive in the incognito
// context, but not the on-the-record context (since the message pipe is
// with the incognito version).
EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
spanning_mode_extension->id()),
testing::UnorderedElementsAre(GetKeepaliveMatcher(
spanning_worker_id, Activity::MESSAGE_PORT)));
EXPECT_THAT(
incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
spanning_mode_extension->id()),
testing::IsEmpty());
EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
split_mode_extension->id()),
testing::IsEmpty());
EXPECT_THAT(
incognito_process_manager->GetServiceWorkerKeepaliveDataForRecords(
split_mode_extension->id()),
testing::UnorderedElementsAre(GetKeepaliveMatcher(
split_incognito_worker_id, Activity::MESSAGE_PORT)));
// Verify the active external request count to validate the above.
EXPECT_EQ(1u, GetExternalRequestCountForWorker(*profile(),
*spanning_mode_extension));
EXPECT_EQ(0u, GetExternalRequestCountForWorker(*incognito_profile,
*spanning_mode_extension));
EXPECT_EQ(
0u, GetExternalRequestCountForWorker(*profile(), *split_mode_extension));
EXPECT_EQ(1u, GetExternalRequestCountForWorker(*incognito_profile,
*split_mode_extension));
// Close the incognito browser while the message channel is still open. Since
// this is the only browser window for the incognito context, this also
// results in the browser context being invalidated.
ProfileDestructionWaiter profile_destruction_waiter(incognito_profile);
TestBrowserClosedWaiter browser_closed_waiter(incognito_browser);
incognito_browser->window()->Close();
ASSERT_TRUE(browser_closed_waiter.WaitUntilClosed());
profile_destruction_waiter.Wait();
// Note: `ProfileDestructionWaiter` only waits for the profile to signal it
// *will* be destroyed. Spin once to finish the job.
base::RunLoop().RunUntilIdle();
// Verify the profile is destroyed.
EXPECT_FALSE(
ExtensionsBrowserClient::Get()->IsValidContext(incognito_profile));
// Verify that all keepalives have been removed, since the message port was
// closed as part of the incognito profile shutdown. (We can't verify
// the incognito values since the incognito profile is destroyed.)
EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
spanning_mode_extension->id()),
testing::IsEmpty());
EXPECT_THAT(process_manager->GetServiceWorkerKeepaliveDataForRecords(
split_mode_extension->id()),
testing::IsEmpty());
EXPECT_EQ(0u, GetExternalRequestCountForWorker(*profile(),
*spanning_mode_extension));
EXPECT_EQ(
0u, GetExternalRequestCountForWorker(*profile(), *split_mode_extension));
}
} // namespace extensions