diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index 9c1c2f9d27e62..8b46e56642bc7 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -5404,6 +5404,8 @@ static_library("browser") { "chromeos/extensions/file_system_provider/file_system_provider_api.h", "chromeos/extensions/file_system_provider/provider_function.cc", "chromeos/extensions/file_system_provider/provider_function.h", + "chromeos/extensions/file_system_provider/service_worker_lifetime_manager.cc", + "chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h", "chromeos/extensions/info_private_api.cc", "chromeos/extensions/info_private_api.h", "chromeos/extensions/login_screen/login/cleanup/cleanup_manager_lacros.cc", diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn index 4755474c309de..81cd68260837b 100644 --- a/chrome/browser/chromeos/BUILD.gn +++ b/chrome/browser/chromeos/BUILD.gn @@ -784,6 +784,8 @@ source_set("chromeos") { "extensions/file_system_provider/file_system_provider_api.h", "extensions/file_system_provider/provider_function.cc", "extensions/file_system_provider/provider_function.h", + "extensions/file_system_provider/service_worker_lifetime_manager.cc", + "extensions/file_system_provider/service_worker_lifetime_manager.h", "extensions/login_screen/login/cleanup/cleanup_manager_ash.cc", "extensions/login_screen/login/cleanup/cleanup_manager_ash.h", "extensions/login_screen/login/cleanup/clipboard_cleanup_handler.cc", @@ -953,6 +955,7 @@ source_set("unit_tests") { "app_mode/app_session_unittest.cc", "app_mode/chrome_kiosk_app_launcher_unittest.cc", "app_mode/kiosk_app_service_launcher_unittest.cc", + "extensions/file_system_provider/service_worker_lifetime_manager_unittest.cc", "extensions/login_screen/login/cleanup/cleanup_manager_unittest.cc", "extensions/login_screen/login/cleanup/extension_cleanup_handler_unittest.cc", "extensions/login_screen/login/cleanup/lacros_cleanup_handler_unittest.cc", diff --git a/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.cc b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.cc new file mode 100644 index 0000000000000..d5567461bd245 --- /dev/null +++ b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.cc @@ -0,0 +1,133 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h" + +#include +#include + +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "extensions/browser/event_router.h" +#include "extensions/browser/process_manager.h" +#include "extensions/browser/process_manager_factory.h" +#include "third_party/blink/public/mojom/service_worker/service_worker_database.mojom-forward.h" + +namespace extensions::file_system_provider { + +bool RequestKey::operator<(const RequestKey& other) const { + return std::tie(extension_id, file_system_id, request_id) < + std::tie(other.extension_id, other.file_system_id, other.request_id); +} + +ServiceWorkerLifetimeManager::ServiceWorkerLifetimeManager( + content::BrowserContext* context) + // Context can be null in tests. + : process_manager_(context ? extensions::ProcessManager::Get(context) + : nullptr) {} + +ServiceWorkerLifetimeManager::~ServiceWorkerLifetimeManager() = default; + +ServiceWorkerLifetimeManager* ServiceWorkerLifetimeManager::Get( + content::BrowserContext* context) { + return ServiceWorkerLifetimeManagerFactory::GetForBrowserContext(context); +} + +void ServiceWorkerLifetimeManager::StartRequest(const RequestKey& key) { + DCHECK(!base::Contains(requests_, key)); + requests_[key] = {}; +} + +void ServiceWorkerLifetimeManager::FinishRequest( + const RequestKey& request_key) { + auto it = requests_.find(request_key); + if (it == requests_.end()) { + return; + } + std::set keepalive_keys = std::move(it->second); + requests_.erase(it); + for (const KeepaliveKey& keepalive_key : keepalive_keys) { + DecrementKeepalive(keepalive_key); + } +} + +void ServiceWorkerLifetimeManager::RequestDispatched( + const RequestKey& key, + const EventTarget& target) { + if (target.service_worker_version_id == + blink::mojom::kInvalidServiceWorkerVersionId) { + return; + } + auto it = requests_.find(key); + if (it == requests_.end()) { + return; + } + std::set& keepalive_keys = it->second; + WorkerId worker_id{ + target.extension_id, + target.render_process_id, + target.service_worker_version_id, + target.worker_thread_id, + }; + std::string uuid = IncrementKeepalive(worker_id); + keepalive_keys.insert(KeepaliveKey{worker_id, uuid}); +} + +void ServiceWorkerLifetimeManager::Shutdown() { + for (const auto& [_, keys] : requests_) { + for (const KeepaliveKey& key : keys) { + DecrementKeepalive(key); + } + } +} + +bool ServiceWorkerLifetimeManager::KeepaliveKey::operator<( + const KeepaliveKey& other) const { + return std::tie(worker_id, request_uuid) < + std::tie(other.worker_id, other.request_uuid); +} + +std::string ServiceWorkerLifetimeManager::IncrementKeepalive( + const WorkerId& worker_id) { + return process_manager_->IncrementServiceWorkerKeepaliveCount( + worker_id, + content::ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout, + extensions::Activity::Type::EVENT, /*extra_data=*/""); +} + +void ServiceWorkerLifetimeManager::DecrementKeepalive(const KeepaliveKey& key) { + process_manager_->DecrementServiceWorkerKeepaliveCount( + key.worker_id, key.request_uuid, extensions::Activity::Type::EVENT, + /*extra_data=*/""); +} + +// static +ServiceWorkerLifetimeManager* +ServiceWorkerLifetimeManagerFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +// static +ServiceWorkerLifetimeManagerFactory* +ServiceWorkerLifetimeManagerFactory::GetInstance() { + return base::Singleton::get(); +} + +ServiceWorkerLifetimeManagerFactory::ServiceWorkerLifetimeManagerFactory() + : BrowserContextKeyedServiceFactory( + "ServiceWorkerLifetimeManagerFactory", + BrowserContextDependencyManager::GetInstance()) { + DependsOn(extensions::ProcessManagerFactory::GetInstance()); +} + +ServiceWorkerLifetimeManagerFactory::~ServiceWorkerLifetimeManagerFactory() = + default; + +KeyedService* ServiceWorkerLifetimeManagerFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new ServiceWorkerLifetimeManager(context); +} + +} // namespace extensions::file_system_provider diff --git a/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h new file mode 100644 index 0000000000000..a2a33fbfa8242 --- /dev/null +++ b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h @@ -0,0 +1,120 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_CHROMEOS_EXTENSIONS_FILE_SYSTEM_PROVIDER_SERVICE_WORKER_LIFETIME_MANAGER_H_ +#define CHROME_BROWSER_CHROMEOS_EXTENSIONS_FILE_SYSTEM_PROVIDER_SERVICE_WORKER_LIFETIME_MANAGER_H_ + +#include +#include +#include + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "components/keyed_service/core/keyed_service.h" +#include "extensions/browser/service_worker/worker_id.h" +#include "extensions/common/extension_id.h" + +namespace content { +class BrowserContext; +} + +namespace extensions { + +class ProcessManager; +struct EventTarget; + +namespace file_system_provider { + +// Identifies a unique fileSystemProvider request: request ID sequence of +// integers tracked per a filesystem instance, or per provider (extension) for +// requests that aren't specific to a filesystem instance. +struct RequestKey { + extensions::ExtensionId extension_id; + std::string file_system_id; + int request_id; + + bool operator<(const RequestKey& other) const; +}; + +// Tracks fileSystemProvider requests that have been dispatched to service +// workers but not replied to yet, and keeps service workers alive while there +// are requests in progress. +class ServiceWorkerLifetimeManager : public KeyedService { + public: + ServiceWorkerLifetimeManager(const ServiceWorkerLifetimeManager&) = delete; + ServiceWorkerLifetimeManager& operator=(const ServiceWorkerLifetimeManager&) = + delete; + ~ServiceWorkerLifetimeManager() override; + + static ServiceWorkerLifetimeManager* Get(content::BrowserContext*); + + // Signals that a request has been sent to a fileSystemProvider. Called when + // the request is about to be dispatched (the actual targets that received the + // request aren't known yet). + void StartRequest(const RequestKey&); + // Signals that a request previously sent to a fileSystemProvider has + // finished. Called either when a request has been replied to (the first + // response finishes the request), or is cancelled, due to timeout or being + // aborted. + void FinishRequest(const RequestKey&); + // Signals that a request has been dispatched to a service worker with + // registered fileSystemProvider listeners. Called for each service worker the + // request has been dispatched to. + void RequestDispatched(const RequestKey&, const EventTarget&); + // KeyedService: + void Shutdown() override; + + protected: + struct KeepaliveKey { + WorkerId worker_id; + std::string request_uuid; + + bool operator==(const KeepaliveKey& other) const; + bool operator<(const KeepaliveKey& other) const; + }; + + explicit ServiceWorkerLifetimeManager(content::BrowserContext*); + + // Virtual for tests. + virtual std::string IncrementKeepalive(const WorkerId&); + virtual void DecrementKeepalive(const KeepaliveKey&); + + private: + friend class ServiceWorkerLifetimeManagerFactory; + FRIEND_TEST_ALL_PREFIXES(ServiceWorkerLifetimeManagerTest, + TestDispatchMultipleEvents); + + raw_ptr process_manager_; + std::map> requests_; +}; + +// KeyedService factory for ServiceWorkerLifetimeManager. +class ServiceWorkerLifetimeManagerFactory + : public BrowserContextKeyedServiceFactory { + public: + ServiceWorkerLifetimeManagerFactory( + const ServiceWorkerLifetimeManagerFactory&) = delete; + ServiceWorkerLifetimeManagerFactory& operator=( + const ServiceWorkerLifetimeManagerFactory&) = delete; + + static ServiceWorkerLifetimeManager* GetForBrowserContext( + content::BrowserContext*); + static ServiceWorkerLifetimeManagerFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits< + ServiceWorkerLifetimeManagerFactory>; + + ServiceWorkerLifetimeManagerFactory(); + ~ServiceWorkerLifetimeManagerFactory() override; + + // BrowserContextKeyedServiceFactory: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; +}; + +} // namespace file_system_provider +} // namespace extensions + +#endif // CHROME_BROWSER_CHROMEOS_EXTENSIONS_FILE_SYSTEM_PROVIDER_SERVICE_WORKER_LIFETIME_MANAGER_H_ diff --git a/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager_unittest.cc b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager_unittest.cc new file mode 100644 index 0000000000000..b7ea89bb80f8a --- /dev/null +++ b/chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager_unittest.cc @@ -0,0 +1,255 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include + +#include "base/strings/stringprintf.h" +#include "chrome/browser/chromeos/extensions/file_system_provider/service_worker_lifetime_manager.h" +#include "extensions/browser/event_router.h" +#include "extensions/browser/service_worker/worker_id.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace extensions::file_system_provider { + +namespace { + +using UuidSet = std::set; +using KeepaliveMap = std::map; + +// Implementation of ServiceWorkerLifetimeManager that stubs out the calls to +// the non-existing ProcessManager in IncrementKeepalive/DecrementKeepalive. +class TestServiceWorkerLifetimeManager : public ServiceWorkerLifetimeManager { + public: + explicit TestServiceWorkerLifetimeManager(KeepaliveMap& keepalive_map) + : ServiceWorkerLifetimeManager(nullptr), keepalive_map_(keepalive_map) {} + + void Reset() { next_keepalive_id_ = 1; } + + private: + std::string IncrementKeepalive(const WorkerId& worker_id) override { + std::string id = base::StringPrintf("uuid-%d", next_keepalive_id_++); + keepalive_map_[worker_id].insert(id); + return id; + } + + void DecrementKeepalive(const KeepaliveKey& key) override { + DCHECK(base::Contains(keepalive_map_, key.worker_id)); + DCHECK(base::Contains(keepalive_map_[key.worker_id], key.request_uuid)); + keepalive_map_[key.worker_id].erase(key.request_uuid); + if (keepalive_map_[key.worker_id].empty()) { + keepalive_map_.erase(key.worker_id); + } + } + + // Effectively emulates ProcessManager. + int next_keepalive_id_ = 1; + KeepaliveMap& keepalive_map_; +}; + +} // namespace + +class ServiceWorkerLifetimeManagerTest : public testing::Test { + public: + void SetUp() override { sw_lifetime_manager_.Reset(); } + + protected: + KeepaliveMap keepalive_map_; + TestServiceWorkerLifetimeManager sw_lifetime_manager_{keepalive_map_}; +}; + +TEST_F(ServiceWorkerLifetimeManagerTest, TestNoDispatch) { + const RequestKey kRequest{ + .extension_id = "ext1", + .file_system_id = "fs1", + .request_id = 1, + }; + + // Keepalive count should not be incremented if a request is never dispatched. + + sw_lifetime_manager_.StartRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); + + sw_lifetime_manager_.FinishRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); +} + +TEST_F(ServiceWorkerLifetimeManagerTest, TestDispatchOneTarget) { + const RequestKey kRequest{ + .extension_id = "ext1", + .file_system_id = "fs1", + .request_id = 1, + }; + const EventTarget kTarget{ + .extension_id = "ext1", + .render_process_id = 1, + .service_worker_version_id = 2, + .worker_thread_id = 3, + }; + const WorkerId kWorkerId{ + .extension_id = "ext1", + .render_process_id = 1, + .version_id = 2, + .thread_id = 3, + }; + + // Simple case: a single request is dispatched and completed. + + sw_lifetime_manager_.StartRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); + + sw_lifetime_manager_.RequestDispatched(kRequest, kTarget); + EXPECT_EQ(keepalive_map_.size(), 1u); + EXPECT_EQ(keepalive_map_[kWorkerId], UuidSet{"uuid-1"}); + + sw_lifetime_manager_.FinishRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); +} + +TEST_F(ServiceWorkerLifetimeManagerTest, TestDispatchMultipleTargets) { + const RequestKey kRequest{ + .extension_id = "ext1", + .file_system_id = "fs1", + .request_id = 1, + }; + auto target = [](int version_id) { + return EventTarget{ + .extension_id = "ext1", + .render_process_id = 1000, + .service_worker_version_id = version_id, + .worker_thread_id = 3, + }; + }; + auto worker = [](int version_id) { + return WorkerId{ + .extension_id = "ext1", + .render_process_id = 1000, + .version_id = version_id, + .thread_id = 3, + }; + }; + + // A request is dispatched to multiple targets in the same extension. + + sw_lifetime_manager_.StartRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); + + sw_lifetime_manager_.RequestDispatched(kRequest, target(1)); + EXPECT_EQ(keepalive_map_.size(), 1u); + EXPECT_EQ(keepalive_map_[worker(1)], UuidSet{"uuid-1"}); + + sw_lifetime_manager_.RequestDispatched(kRequest, target(2)); + EXPECT_EQ(keepalive_map_.size(), 2u); + EXPECT_EQ(keepalive_map_[worker(1)], UuidSet{"uuid-1"}); + EXPECT_EQ(keepalive_map_[worker(2)], UuidSet{"uuid-2"}); + + // Finishing a request clears out any keepalive references associated with + // this request. + + sw_lifetime_manager_.FinishRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); +} + +TEST_F(ServiceWorkerLifetimeManagerTest, TestDispatchLate) { + const RequestKey kRequest{ + .extension_id = "ext1", + .file_system_id = "fs1", + .request_id = 1, + }; + + sw_lifetime_manager_.StartRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); + + sw_lifetime_manager_.FinishRequest(kRequest); + EXPECT_EQ(keepalive_map_.size(), 0u); + + sw_lifetime_manager_.RequestDispatched(kRequest, + EventTarget{ + .extension_id = "ext1", + .render_process_id = 1, + .service_worker_version_id = 2, + .worker_thread_id = 3, + }); + EXPECT_EQ(keepalive_map_.size(), 0u); +} + +TEST_F(ServiceWorkerLifetimeManagerTest, TestDispatchMultipleEvents) { + const EventTarget kTarget1{ + .extension_id = "ext1", + .render_process_id = 1000, + .service_worker_version_id = 2, + .worker_thread_id = 3, + }; + const EventTarget kTarget2{ + .extension_id = "ext2", + .render_process_id = 1001, + .service_worker_version_id = 4, + .worker_thread_id = 5, + }; + const WorkerId kWorkerId1{ + .extension_id = "ext1", + .render_process_id = 1000, + .version_id = 2, + .thread_id = 3, + }; + const WorkerId kWorkerId2{ + .extension_id = "ext2", + .render_process_id = 1001, + .version_id = 4, + .thread_id = 5, + }; + + // Send four requests: + // - two different extensions, + // - two separate file system instances in the same extension, + // - two requests to the same file system instance. + sw_lifetime_manager_.StartRequest(RequestKey{"ext1", "fs1-1", 1}); + sw_lifetime_manager_.StartRequest(RequestKey{"ext1", "fs1-2", 1}); + sw_lifetime_manager_.StartRequest(RequestKey{"ext1", "fs1-1", 2}); + sw_lifetime_manager_.StartRequest(RequestKey{"ext2", "fs2-1", 1}); + sw_lifetime_manager_.RequestDispatched(RequestKey{"ext1", "fs1-1", 1}, + kTarget1); + sw_lifetime_manager_.RequestDispatched(RequestKey{"ext1", "fs1-2", 1}, + kTarget1); + sw_lifetime_manager_.RequestDispatched(RequestKey{"ext1", "fs1-1", 2}, + kTarget1); + sw_lifetime_manager_.RequestDispatched(RequestKey{"ext2", "fs2-1", 1}, + kTarget2); + + // Three events incremented one service worker's keepalive three times, and + // one event for another service worker. + ASSERT_EQ(keepalive_map_.size(), 2u); + UuidSet expectedWorker1{"uuid-1", "uuid-2", "uuid-3"}; + UuidSet expectedWorker2{"uuid-4"}; + EXPECT_EQ(keepalive_map_[kWorkerId1], expectedWorker1); + EXPECT_EQ(keepalive_map_[kWorkerId2], expectedWorker2); + + // As requests finish, the worker's keepalive references keep getting removed + // until none left. + + sw_lifetime_manager_.FinishRequest(RequestKey{"ext1", "fs1-1", 1}); + + ASSERT_EQ(keepalive_map_.size(), 2u); + expectedWorker1 = {"uuid-2", "uuid-3"}; + EXPECT_EQ(keepalive_map_[kWorkerId1], expectedWorker1); + EXPECT_EQ(keepalive_map_[kWorkerId2], expectedWorker2); + + sw_lifetime_manager_.FinishRequest(RequestKey{"ext1", "fs1-1", 2}); + + ASSERT_EQ(keepalive_map_.size(), 2u); + expectedWorker1 = {"uuid-2"}; + EXPECT_EQ(keepalive_map_[kWorkerId1], expectedWorker1); + EXPECT_EQ(keepalive_map_[kWorkerId2], expectedWorker2); + + sw_lifetime_manager_.FinishRequest(RequestKey{"ext1", "fs1-2", 1}); + + ASSERT_EQ(keepalive_map_.size(), 1u); + EXPECT_EQ(keepalive_map_[kWorkerId2], expectedWorker2); + + sw_lifetime_manager_.FinishRequest(RequestKey{"ext2", "fs2-1", 1}); + + ASSERT_EQ(keepalive_map_.size(), 0u); +} + +} // namespace extensions::file_system_provider