From c2dbd5298239f9f942c93311b58552de8b8f5760 Mon Sep 17 00:00:00 2001 From: Luke Wilde Date: Thu, 21 May 2026 15:33:23 +0200 Subject: [PATCH] LibWebView+WebContent+WebWorker: Move worker ownership into LibWebView This lays the groundwork to allow shared workers, nested workers (i.e. workers owned by workers) and service workers to function independently of WebContent. --- Libraries/LibWeb/CMakeLists.txt | 1 + Libraries/LibWeb/HTML/SharedWorker.cpp | 100 +--- Libraries/LibWeb/HTML/WorkerAgentForward.h | 18 + Libraries/LibWeb/HTML/WorkerAgentParent.cpp | 183 ++++---- Libraries/LibWeb/HTML/WorkerAgentParent.h | 19 +- Libraries/LibWeb/HTML/WorkerAgentTypes.cpp | 48 ++ Libraries/LibWeb/HTML/WorkerAgentTypes.h | 47 ++ Libraries/LibWeb/Page/Page.h | 9 +- Libraries/LibWeb/Worker/WebWorkerClient.cpp | 27 +- Libraries/LibWeb/Worker/WebWorkerClient.h | 15 +- Libraries/LibWeb/Worker/WebWorkerClient.ipc | 7 +- Libraries/LibWeb/Worker/WebWorkerServer.ipc | 9 + Libraries/LibWebView/CMakeLists.txt | 1 + Libraries/LibWebView/WebContentClient.cpp | 19 +- Libraries/LibWebView/WebContentClient.h | 4 +- Libraries/LibWebView/WorkerProcessManager.cpp | 429 ++++++++++++++++++ Libraries/LibWebView/WorkerProcessManager.h | 113 +++++ Services/WebContent/ConnectionFromClient.cpp | 28 +- Services/WebContent/ConnectionFromClient.h | 5 + Services/WebContent/PageClient.cpp | 13 +- Services/WebContent/PageClient.h | 3 +- Services/WebContent/WebContentClient.ipc | 4 +- Services/WebContent/WebContentServer.ipc | 6 + Services/WebWorker/ConnectionFromClient.cpp | 47 +- Services/WebWorker/ConnectionFromClient.h | 6 + Services/WebWorker/PageHost.cpp | 15 +- Services/WebWorker/PageHost.h | 5 +- Services/WebWorker/WorkerHost.cpp | 107 ++++- Services/WebWorker/WorkerHost.h | 23 + .../Worker/SharedWorker-close-one-owner.txt | 2 + .../expected/Worker/SharedWorker-reuse.txt | 4 + .../Worker/SharedWorker-type-mismatch.txt | 2 + .../Worker/SharedWorker-close-one-owner.html | 59 +++ .../Text/input/Worker/SharedWorker-reuse.html | 46 ++ .../Worker/SharedWorker-type-mismatch.html | 51 +++ 35 files changed, 1212 insertions(+), 263 deletions(-) create mode 100644 Libraries/LibWeb/HTML/WorkerAgentForward.h create mode 100644 Libraries/LibWeb/HTML/WorkerAgentTypes.cpp create mode 100644 Libraries/LibWeb/HTML/WorkerAgentTypes.h create mode 100644 Libraries/LibWebView/WorkerProcessManager.cpp create mode 100644 Libraries/LibWebView/WorkerProcessManager.h create mode 100644 Tests/LibWeb/Text/expected/Worker/SharedWorker-close-one-owner.txt create mode 100644 Tests/LibWeb/Text/expected/Worker/SharedWorker-reuse.txt create mode 100644 Tests/LibWeb/Text/expected/Worker/SharedWorker-type-mismatch.txt create mode 100644 Tests/LibWeb/Text/input/Worker/SharedWorker-close-one-owner.html create mode 100644 Tests/LibWeb/Text/input/Worker/SharedWorker-reuse.html create mode 100644 Tests/LibWeb/Text/input/Worker/SharedWorker-type-mismatch.html diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 2c24d23d46..aaf6a6871f 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -721,6 +721,7 @@ set(SOURCES HTML/WorkerDebugConsoleClient.cpp HTML/WorkerGlobalScope.cpp HTML/WorkerLocation.cpp + HTML/WorkerAgentTypes.cpp HTML/WorkerNavigator.cpp HTML/WorkletGlobalScope.cpp HTML/XMLSerializer.cpp diff --git a/Libraries/LibWeb/HTML/SharedWorker.cpp b/Libraries/LibWeb/HTML/SharedWorker.cpp index 04bc1cfef1..91a4cbbb11 100644 --- a/Libraries/LibWeb/HTML/SharedWorker.cpp +++ b/Libraries/LibWeb/HTML/SharedWorker.cpp @@ -4,7 +4,6 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include #include #include #include @@ -14,10 +13,8 @@ #include #include #include -#include #include #include -#include #include #include @@ -67,104 +64,13 @@ WebIDL::ExceptionOr> SharedWorker::construct_impl(JS::Real // 7. Set this's port to outsidePort. auto worker = realm.create(realm, url.release_value(), options, outside_port); - // 8. Let callerIsSecureContext be true if outside settings is a secure context; otherwise, false. - auto caller_is_secure_context = HTML::is_secure_context(outside_settings); - - // 9. Let outsideStorageKey be the result of running obtain a storage key for non-storage purposes given outsideSettings. - auto outside_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(outside_settings); - // 10. Let worker be this. // NB: This is done earlier. // 11. Enqueue the following steps to the shared worker manager: - // FIXME: "A user agent has an associated shared worker manager which is the result of starting a new parallel queue." - // We just use the singular global event loop for now. - Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [worker, outside_port, &outside_settings, caller_is_secure_context, outside_storage_key = move(outside_storage_key)]() mutable { - // 1. Let workerGlobalScope be null. - GC::Ptr worker_global_scope; - - // 2. For each scope in the list of all SharedWorkerGlobalScope objects: - for (auto& scope : all_shared_worker_global_scopes()) { - // 1. Let workerStorageKey be the result of running obtain a storage key for non-storage purposes given - // scope's relevant settings object. - auto worker_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(HTML::relevant_settings_object(scope)); - - // 2. If all of the following are true: - if ( - // * workerStorageKey equals outsideStorageKey; - worker_storage_key == outside_storage_key - - // * scope's closing flag is false; - && !scope->is_closing() - - // * scope's constructor URL equals urlRecord; and - && scope->url() == worker->m_script_url - - // * scope's name equals options["name"], - && scope->name() == worker->m_options.name) - // then: - { - // 1. Set workerGlobalScope to scope. - worker_global_scope = scope; - - // 2. Break. - break; - } - } - - // FIXME: 3. If workerGlobalScope is not null, but the user agent has been configured to disallow communication between the worker represented by the workerGlobalScope and the scripts whose settings object is outsideSettings, then set workerGlobalScope to null. - // FIXME: 4. If workerGlobalScope is not null, and any of the following are true: ... - - // 5. If workerGlobalScope is not null: - if (worker_global_scope) { - // 1. Let insideSettings be workerGlobalScope's relevant settings object. - auto& inside_settings = relevant_settings_object(*worker_global_scope); - - // 2. Let workerIsSecureContext be true if insideSettings is a secure context; otherwise, false. - auto worker_is_secure_context = is_secure_context(inside_settings); - - // 3. If workerIsSecureContext is not callerIsSecureContext: - if (worker_is_secure_context != caller_is_secure_context) { - // 1. Queue a global task on the DOM manipulation task source given worker's relevant global object to fire an event named error at worker. - queue_global_task(Task::Source::DOMManipulation, relevant_global_object(worker), GC::create_function(worker->heap(), [worker]() { - worker->dispatch_event(DOM::Event::create(worker->realm(), EventNames::error)); - })); - - // 2. Abort these steps. - return; - } - - // FIXME: 4. Associate worker with workerGlobalScope. - - // 5. Let insidePort be a new MessagePort in insideSettings's realm. - auto inside_port = HTML::MessagePort::create(inside_settings.realm()); - - // 6. Entangle outsidePort and insidePort. - outside_port->entangle_with(inside_port); - - // 7. Queue a global task on the DOM manipulation task source given workerGlobalScope to fire an event - // named connect at workerGlobalScope, using MessageEvent, with the data attribute initialized to the - // empty string, the ports attribute initialized to a new frozen array containing only insidePort, and - // the source attribute initialized to insidePort. - queue_global_task(Task::Source::DOMManipulation, *worker_global_scope, GC::create_function(worker->heap(), [worker_global_scope, inside_port]() { - auto& realm = worker_global_scope->realm(); - - Bindings::MessageEventInit init; - init.data = JS::PrimitiveString::create(realm.vm(), String {}); - init.ports.append(inside_port); - init.source = inside_port; - - worker_global_scope->dispatch_event(MessageEvent::create(realm, EventNames::connect, init)); - })); - - // FIXME: 8. Append the relevant owner to add given outsideSettings to workerGlobalScope's owner set. - - } - // 6. Otherwise, in parallel, run a worker given worker, urlRecord, outsideSettings, outsidePort, and options. - else { - run_a_worker(worker, worker->m_script_url, outside_settings, outside_port, worker->m_options); - } - })); + // The actual shared worker manager is owned by the UI process. It will either start a new + // WebWorker agent or transfer this port to an existing same-key SharedWorkerGlobalScope. + run_a_worker(worker, worker->m_script_url, outside_settings, outside_port, worker->m_options); return worker; } diff --git a/Libraries/LibWeb/HTML/WorkerAgentForward.h b/Libraries/LibWeb/HTML/WorkerAgentForward.h new file mode 100644 index 0000000000..3b90d35728 --- /dev/null +++ b/Libraries/LibWeb/HTML/WorkerAgentForward.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::HTML { + +using WorkerAgentId = u64; +using WorkerAgentOwnerToken = u64; + +struct WorkerAgentStartRequest; + +} diff --git a/Libraries/LibWeb/HTML/WorkerAgentParent.cpp b/Libraries/LibWeb/HTML/WorkerAgentParent.cpp index 33aa398527..60c6a8e0e5 100644 --- a/Libraries/LibWeb/HTML/WorkerAgentParent.cpp +++ b/Libraries/LibWeb/HTML/WorkerAgentParent.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -18,16 +19,23 @@ #include #include #include -#include +#include namespace Web::HTML { GC_DEFINE_ALLOCATOR(WorkerAgentParent); +static HashMap>& worker_agent_parents() +{ + static HashMap> map; + return map; +} + WorkerAgentParent::WorkerAgentParent(URL::URL url, Bindings::WorkerOptions const& options, GC::Ptr outside_port, GC::Ref outside_settings, GC::Ref worker_event_target, Bindings::AgentType agent_type) : m_worker_options(options) , m_agent_type(agent_type) , m_url(move(url)) + , m_owner_token(next_owner_token()) , m_outside_port(outside_port) , m_outside_settings(outside_settings) , m_worker_event_target(worker_event_target) @@ -51,90 +59,61 @@ void WorkerAgentParent::initialize(JS::Realm& realm) // so let's do it here for now. m_outside_port->start(); - // NOTE: This blocking IPC call may launch another process. - // If spinning the event loop for this can cause other javascript to execute, we're in trouble. - auto response = Bindings::principal_host_defined_page(realm).client().request_worker_agent(m_agent_type); - - auto transport = MUST(response.worker_handle.create_transport()); - m_worker_ipc = make_ref_counted(move(transport)); - setup_worker_ipc_callbacks(realm); - - m_worker_ipc->async_connect_to_request_server(move(response.request_server_handle)); - m_worker_ipc->async_connect_to_image_decoder(move(response.image_decoder_handle)); - auto serialized_outside_settings = m_outside_settings->serialize(); - m_worker_ipc->async_start_worker(m_url, m_worker_options.type, m_worker_options.credentials, m_worker_options.name, move(data_holder), serialized_outside_settings, m_agent_type); + // 8. Let callerIsSecureContext be true if outside settings is a secure context; otherwise, false. + // 9. Let outsideStorageKey be the result of running obtain a storage key for non-storage purposes + // given outsideSettings. + WorkerAgentStartRequest request { + .url = m_url, + .agent_type = m_agent_type, + .type = m_worker_options.type, + .credentials = m_worker_options.credentials, + .name = m_worker_options.name, + .outside_port = move(data_holder), + .outside_settings = serialized_outside_settings, + .storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(*m_outside_settings), + .caller_is_secure_context = is_secure_context(*m_outside_settings), + .owner_token = m_owner_token, + }; + + // NOTE: This blocking IPC call may launch another process. + // If spinning the event loop for this can cause other javascript to execute, we're in trouble. + worker_agent_parents().set(m_owner_token, *this); + m_agent_id = Bindings::principal_host_defined_page(realm).client().start_worker_agent(move(request)); } -void WorkerAgentParent::setup_worker_ipc_callbacks(JS::Realm& realm) +void WorkerAgentParent::did_finish_loading_worker_script(WorkerAgentOwnerToken owner_token) { - // NOTE: As long as WorkerAgentParent is alive, realm and m_worker_ipc will be alive. - m_worker_ipc->on_request_cookie = [realm = GC::RawRef { realm }](URL::URL const& url, HTTP::Cookie::Source source) { - auto& client = Bindings::principal_host_defined_page(realm).client(); - return client.page_did_request_cookie(url, source); - }; - m_worker_ipc->on_post_broadcast_channel_message = [realm = GC::RawRef { realm }](Web::HTML::BroadcastChannelMessage message) { - auto& client = Bindings::principal_host_defined_page(realm).client(); - client.page_did_post_broadcast_channel_message(message); - }; - m_worker_ipc->on_request_worker_agent = [realm = GC::RawRef { realm }](Web::Bindings::AgentType worker_type) -> Messages::WebWorkerClient::RequestWorkerAgentResponse { - auto& client = Bindings::principal_host_defined_page(realm).client(); - auto response = client.request_worker_agent(worker_type); - return { move(response.worker_handle), move(response.request_server_handle), move(response.image_decoder_handle) }; - }; - m_worker_ipc->on_worker_exception = [self = GC::Weak { *this }](String message, String filename, u32 lineno, u32 colno) { - if (!self) - return; - // https://html.spec.whatwg.org/multipage/webappapis.html#report-an-exception - // 7.2: If global implements DedicatedWorkerGlobalScope, queue a global task on the DOM manipulation task source with the global's associated Worker's relevant global object to run these steps: - auto outside_settings = GC::Ref { *self->m_outside_settings }; - auto worker_event_target = GC::Ref { *self->m_worker_event_target }; - queue_global_task(Task::Source::DOMManipulation, outside_settings->global_object(), GC::create_function(outside_settings->heap(), [outside_settings, worker_event_target, message = move(message), filename = move(filename), lineno, colno]() { - // 1. Let workerObject be the Worker object associated with global. - auto& worker_object = as(*worker_event_target); + auto parent = worker_agent_parents().find(owner_token); + if (parent == worker_agent_parents().end()) + return; + parent->value->release_startup_keep_alive(); +} - auto& realm = outside_settings->realm(); +void WorkerAgentParent::did_fail_loading_worker_script(WorkerAgentOwnerToken owner_token) +{ + auto parent = worker_agent_parents().find(owner_token); + if (parent == worker_agent_parents().end()) + return; + parent->value->dispatch_error_event(); + parent->value->release_startup_keep_alive(); +} - // 2. Set notHandled to the result of firing an event named error at workerObject, using ErrorEvent, with the - // cancelable attribute initialized to true, and additional attributes initialized according to errorInfo. - Bindings::ErrorEventInit event_init {}; - event_init.cancelable = true; - event_init.message = message; - event_init.filename = filename; - event_init.lineno = lineno; - event_init.colno = colno; - event_init.error = JS::js_null(); - auto error = ErrorEvent::create(realm, EventNames::error, event_init); - bool not_handled = worker_object.dispatch_event(error); +void WorkerAgentParent::did_report_worker_exception(WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) +{ + auto parent = worker_agent_parents().find(owner_token); + if (parent == worker_agent_parents().end()) + return; + parent->value->dispatch_worker_exception(move(message), move(filename), lineno, colno); +} - // 3. If notHandled is true, then report exception for workerObject's relevant global object with omitError set to true. - if (not_handled) - as(outside_settings->global_object()).report_an_exception(error, WindowOrWorkerGlobalScopeMixin::OmitError::Yes); - })); - }; - m_worker_ipc->on_worker_close = [self = GC::Weak { *this }]() { - if (!self) - return; - self->release_startup_keep_alive(); - }; - m_worker_ipc->on_worker_script_load_success = [self = GC::Weak { *this }]() { - if (!self) - return; - self->release_startup_keep_alive(); - }; - m_worker_ipc->on_worker_script_load_failure = [self = GC::Weak { *this }]() { - if (!self) - return; - auto outside_settings = GC::Ref { *self->m_outside_settings }; - auto event_target = GC::Ref { *self->m_worker_event_target }; - // See: https://html.spec.whatwg.org/multipage/workers.html#worker-processing-model, onComplete handler for fetching script. - // 1. Queue a global task on the DOM manipulation task source given worker's relevant global object to fire an event named error at worker. - queue_global_task(Task::Source::DOMManipulation, outside_settings->global_object(), GC::create_function(outside_settings->heap(), [event_target, outside_settings]() { - event_target->dispatch_event(DOM::Event::create(outside_settings->realm(), EventNames::error)); - })); - self->release_startup_keep_alive(); - }; +void WorkerAgentParent::did_close_worker(WorkerAgentOwnerToken owner_token) +{ + auto parent = worker_agent_parents().find(owner_token); + if (parent == worker_agent_parents().end()) + return; + parent->value->release_startup_keep_alive(); } void WorkerAgentParent::release_startup_keep_alive() @@ -142,6 +121,56 @@ void WorkerAgentParent::release_startup_keep_alive() m_outside_settings->release_worker_agent_from_startup_keep_alive(*this); } +void WorkerAgentParent::dispatch_error_event() +{ + // See: https://html.spec.whatwg.org/multipage/workers.html#worker-processing-model, onComplete handler for fetching script. + // 1. Queue a global task on the DOM manipulation task source given worker's relevant global object to fire an event named error at worker. + queue_global_task(Task::Source::DOMManipulation, m_outside_settings->global_object(), GC::create_function(m_outside_settings->heap(), [this] { + m_worker_event_target->dispatch_event(DOM::Event::create(m_outside_settings->realm(), EventNames::error)); + })); +} + +void WorkerAgentParent::dispatch_worker_exception(String message, String filename, u32 lineno, u32 colno) +{ + // https://html.spec.whatwg.org/multipage/webappapis.html#report-an-exception + // 7.2: If global implements DedicatedWorkerGlobalScope, queue a global task on the DOM manipulation task source with the global's associated Worker's relevant global object to run these steps: + queue_global_task(Task::Source::DOMManipulation, m_outside_settings->global_object(), GC::create_function(m_outside_settings->heap(), [this, message = move(message), filename = move(filename), lineno, colno]() { + auto& realm = m_outside_settings->realm(); + + // 2. Set notHandled to the result of firing an event named error at workerObject, using ErrorEvent, with the + // cancelable attribute initialized to true, and additional attributes initialized according to errorInfo. + Bindings::ErrorEventInit event_init {}; + event_init.cancelable = true; + event_init.message = message; + event_init.filename = filename; + event_init.lineno = lineno; + event_init.colno = colno; + event_init.error = JS::js_null(); + auto error = ErrorEvent::create(realm, EventNames::error, event_init); + bool not_handled = m_worker_event_target->dispatch_event(error); + + // 3. If notHandled is true, then report exception for workerObject's relevant global object with omitError set to true. + if (not_handled) + as(m_outside_settings->global_object()).report_an_exception(error, WindowOrWorkerGlobalScopeMixin::OmitError::Yes); + })); +} + +WorkerAgentOwnerToken WorkerAgentParent::next_owner_token() +{ + static WorkerAgentOwnerToken s_next_owner_token = 0; + return ++s_next_owner_token; +} + +void WorkerAgentParent::finalize() +{ + Base::finalize(); + + worker_agent_parents().remove(m_owner_token); + + if (m_agent_id != 0) + Bindings::principal_host_defined_page(m_outside_settings->realm()).client().close_worker_agent(m_agent_id, m_owner_token); +} + void WorkerAgentParent::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); diff --git a/Libraries/LibWeb/HTML/WorkerAgentParent.h b/Libraries/LibWeb/HTML/WorkerAgentParent.h index 01d87f9ed1..71c7d6ada6 100644 --- a/Libraries/LibWeb/HTML/WorkerAgentParent.h +++ b/Libraries/LibWeb/HTML/WorkerAgentParent.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace Web::HTML { @@ -19,25 +20,37 @@ class WorkerAgentParent : public JS::Cell { GC_CELL(WorkerAgentParent, JS::Cell); GC_DECLARE_ALLOCATOR(WorkerAgentParent); +public: + static constexpr bool OVERRIDES_FINALIZE = true; + + static WEB_API void did_finish_loading_worker_script(WorkerAgentOwnerToken); + static WEB_API void did_fail_loading_worker_script(WorkerAgentOwnerToken); + static WEB_API void did_report_worker_exception(WorkerAgentOwnerToken, String message, String filename, u32 lineno, u32 colno); + static WEB_API void did_close_worker(WorkerAgentOwnerToken); + protected: WorkerAgentParent(URL::URL url, Bindings::WorkerOptions const& options, GC::Ptr outside_port, GC::Ref outside_settings, GC::Ref worker_event_target, Bindings::AgentType); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; + virtual void finalize() override; private: - void setup_worker_ipc_callbacks(JS::Realm&); void release_startup_keep_alive(); + void dispatch_error_event(); + void dispatch_worker_exception(String message, String filename, u32 lineno, u32 colno); + + static WorkerAgentOwnerToken next_owner_token(); Bindings::WorkerOptions m_worker_options; Bindings::AgentType m_agent_type { Bindings::AgentType::DedicatedWorker }; URL::URL m_url; + WorkerAgentId m_agent_id { 0 }; + WorkerAgentOwnerToken m_owner_token { 0 }; GC::Ptr m_message_port; GC::Ptr m_outside_port; GC::Ref m_outside_settings; GC::Ref m_worker_event_target; - - RefPtr m_worker_ipc; }; } diff --git a/Libraries/LibWeb/HTML/WorkerAgentTypes.cpp b/Libraries/LibWeb/HTML/WorkerAgentTypes.cpp new file mode 100644 index 0000000000..1e569906e6 --- /dev/null +++ b/Libraries/LibWeb/HTML/WorkerAgentTypes.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace IPC { + +template<> +ErrorOr encode(Encoder& encoder, Web::HTML::WorkerAgentStartRequest const& request) +{ + TRY(encoder.encode(request.url)); + TRY(encoder.encode(request.agent_type)); + TRY(encoder.encode(request.type)); + TRY(encoder.encode(request.credentials)); + TRY(encoder.encode(request.name)); + TRY(encoder.encode(request.extended_lifetime)); + TRY(encoder.encode(request.outside_port)); + TRY(encoder.encode(request.outside_settings)); + TRY(encoder.encode(request.storage_key)); + TRY(encoder.encode(request.caller_is_secure_context)); + TRY(encoder.encode(request.owner_token)); + return {}; +} + +template<> +ErrorOr decode(Decoder& decoder) +{ + return Web::HTML::WorkerAgentStartRequest { + .url = TRY(decoder.decode()), + .agent_type = TRY(decoder.decode()), + .type = TRY(decoder.decode()), + .credentials = TRY(decoder.decode()), + .name = TRY(decoder.decode()), + .extended_lifetime = TRY(decoder.decode()), + .outside_port = TRY(decoder.decode()), + .outside_settings = TRY(decoder.decode()), + .storage_key = TRY(decoder.decode()), + .caller_is_secure_context = TRY(decoder.decode()), + .owner_token = TRY(decoder.decode()), + }; +} + +} diff --git a/Libraries/LibWeb/HTML/WorkerAgentTypes.h b/Libraries/LibWeb/HTML/WorkerAgentTypes.h new file mode 100644 index 0000000000..4e70daa852 --- /dev/null +++ b/Libraries/LibWeb/HTML/WorkerAgentTypes.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +struct WEB_API WorkerAgentStartRequest { + URL::URL url; + Bindings::AgentType agent_type { Bindings::AgentType::DedicatedWorker }; + Bindings::WorkerType type { Bindings::WorkerType::Classic }; + Bindings::RequestCredentials credentials { Bindings::RequestCredentials::SameOrigin }; + String name; + // FIXME: We don't implement SharedWorkerOptions/extendedLifetime yet. + bool extended_lifetime { false }; + TransferDataEncoder outside_port; + SerializedEnvironmentSettingsObject outside_settings; + StorageAPI::StorageKey storage_key; + bool caller_is_secure_context { false }; + WorkerAgentOwnerToken owner_token { 0 }; +}; + +} + +namespace IPC { + +template<> +WEB_API ErrorOr encode(Encoder&, Web::HTML::WorkerAgentStartRequest const&); + +template<> +WEB_API ErrorOr decode(Decoder&); + +} diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index 52c47c857f..f14f34f0b2 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -523,12 +524,8 @@ public: virtual void page_did_report_worker_exception([[maybe_unused]] String const& message, [[maybe_unused]] String const& filename, [[maybe_unused]] u32 lineno, [[maybe_unused]] u32 colno) { } virtual void page_did_post_broadcast_channel_message([[maybe_unused]] HTML::BroadcastChannelMessage const& message) { } - struct WorkerAgentResponse { - IPC::TransportHandle worker_handle; - IPC::TransportHandle request_server_handle; - IPC::TransportHandle image_decoder_handle; - }; - virtual WorkerAgentResponse request_worker_agent([[maybe_unused]] Web::Bindings::AgentType worker_type) { return {}; } + virtual HTML::WorkerAgentId start_worker_agent([[maybe_unused]] HTML::WorkerAgentStartRequest&& request) { return {}; } + virtual void close_worker_agent([[maybe_unused]] HTML::WorkerAgentId agent_id, [[maybe_unused]] HTML::WorkerAgentOwnerToken owner_token) { } virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr previous_sibling, [[maybe_unused]] GC::Ptr next_sibling, [[maybe_unused]] Optional const& attribute_name) { } diff --git a/Libraries/LibWeb/Worker/WebWorkerClient.cpp b/Libraries/LibWeb/Worker/WebWorkerClient.cpp index 3e85974798..49f142e7f3 100644 --- a/Libraries/LibWeb/Worker/WebWorkerClient.cpp +++ b/Libraries/LibWeb/Worker/WebWorkerClient.cpp @@ -12,7 +12,8 @@ HashTable WebWorkerClient::s_all_clients; void WebWorkerClient::die() { - // FIXME: Notify WorkerAgent that the worker is dead + if (on_worker_died) + on_worker_died(); } void WebWorkerClient::did_close_worker() @@ -21,10 +22,10 @@ void WebWorkerClient::did_close_worker() on_worker_close(); } -void WebWorkerClient::did_finish_loading_worker_script() +void WebWorkerClient::did_finish_loading_worker_script(bool worker_is_secure_context) { if (on_worker_script_load_success) - on_worker_script_load_success(); + on_worker_script_load_success(worker_is_secure_context); } void WebWorkerClient::did_fail_loading_worker_script() @@ -46,17 +47,29 @@ Messages::WebWorkerClient::DidRequestCookieResponse WebWorkerClient::did_request return HTTP::Cookie::VersionedCookie {}; } +void WebWorkerClient::did_request_file(ByteString path, i32 request_id) +{ + if (on_request_file) + on_request_file(move(path), request_id); +} + void WebWorkerClient::did_post_broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) { if (on_post_broadcast_channel_message) on_post_broadcast_channel_message(move(message)); } -Messages::WebWorkerClient::RequestWorkerAgentResponse WebWorkerClient::request_worker_agent(Web::Bindings::AgentType worker_type) +Messages::WebWorkerClient::StartWorkerAgentResponse WebWorkerClient::start_worker_agent(Web::HTML::WorkerAgentStartRequest request) { - if (on_request_worker_agent) - return on_request_worker_agent(worker_type); - return { IPC::TransportHandle {}, IPC::TransportHandle {}, IPC::TransportHandle {} }; + if (on_start_worker_agent) + return on_start_worker_agent(move(request)); + return { 0 }; +} + +void WebWorkerClient::close_worker_agent(Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) +{ + if (on_close_worker_agent) + on_close_worker_agent(agent_id, owner_token); } WebWorkerClient::WebWorkerClient(NonnullOwnPtr transport) diff --git a/Libraries/LibWeb/Worker/WebWorkerClient.h b/Libraries/LibWeb/Worker/WebWorkerClient.h index 4d412d0f5e..1b100df0f1 100644 --- a/Libraries/LibWeb/Worker/WebWorkerClient.h +++ b/Libraries/LibWeb/Worker/WebWorkerClient.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include @@ -35,20 +37,25 @@ public: void set_pid(pid_t pid) { m_pid = pid; } virtual void did_close_worker() override; - virtual void did_finish_loading_worker_script() override; + virtual void did_finish_loading_worker_script(bool worker_is_secure_context) override; virtual void did_fail_loading_worker_script() override; virtual void did_report_worker_exception(String message, String filename, u32 lineno, u32 colno) override; virtual Messages::WebWorkerClient::DidRequestCookieResponse did_request_cookie(URL::URL, HTTP::Cookie::Source) override; + virtual void did_request_file(ByteString path, i32 request_id) override; virtual void did_post_broadcast_channel_message(Web::HTML::BroadcastChannelMessage) override; - virtual Messages::WebWorkerClient::RequestWorkerAgentResponse request_worker_agent(Web::Bindings::AgentType worker_type) override; + virtual Messages::WebWorkerClient::StartWorkerAgentResponse start_worker_agent(Web::HTML::WorkerAgentStartRequest request) override; + virtual void close_worker_agent(Web::HTML::WorkerAgentId, Web::HTML::WorkerAgentOwnerToken) override; Function on_worker_close; - Function on_worker_script_load_success; + Function on_worker_died; + Function on_worker_script_load_success; Function on_worker_script_load_failure; Function on_worker_exception; Function on_request_cookie; + Function on_request_file; Function on_post_broadcast_channel_message; - Function on_request_worker_agent; + Function on_start_worker_agent; + Function on_close_worker_agent; private: virtual void die() override; diff --git a/Libraries/LibWeb/Worker/WebWorkerClient.ipc b/Libraries/LibWeb/Worker/WebWorkerClient.ipc index 434bc4f8b7..4473492da2 100644 --- a/Libraries/LibWeb/Worker/WebWorkerClient.ipc +++ b/Libraries/LibWeb/Worker/WebWorkerClient.ipc @@ -3,13 +3,16 @@ #include #include #include +#include endpoint WebWorkerClient { did_close_worker() =| - did_finish_loading_worker_script() =| + did_finish_loading_worker_script(bool worker_is_secure_context) =| did_fail_loading_worker_script() =| did_report_worker_exception(String message, String filename, u32 lineno, u32 colno) =| did_request_cookie(URL::URL url, HTTP::Cookie::Source source) => (HTTP::Cookie::VersionedCookie cookie) + did_request_file(ByteString path, i32 request_id) =| did_post_broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) =| - request_worker_agent(Web::Bindings::AgentType worker_type) => (IPC::TransportHandle handle, IPC::TransportHandle request_server_handle, IPC::TransportHandle image_decoder_handle) + start_worker_agent(Web::HTML::WorkerAgentStartRequest request) => (Web::HTML::WorkerAgentId agent_id) + close_worker_agent(Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) =| } diff --git a/Libraries/LibWeb/Worker/WebWorkerServer.ipc b/Libraries/LibWeb/Worker/WebWorkerServer.ipc index d80800a525..c0f5428e73 100644 --- a/Libraries/LibWeb/Worker/WebWorkerServer.ipc +++ b/Libraries/LibWeb/Worker/WebWorkerServer.ipc @@ -6,6 +6,7 @@ #include #include #include +#include endpoint WebWorkerServer { @@ -20,8 +21,16 @@ endpoint WebWorkerServer { Web::HTML::SerializedEnvironmentSettingsObject outside_settings, Web::Bindings::AgentType agent_type) =| + connect_shared_worker(Web::HTML::TransferDataEncoder message_port, + Web::HTML::SerializedEnvironmentSettingsObject outside_settings) =| + close_worker() =| + did_worker_agent_finish_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) =| + did_worker_agent_fail_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) =| + did_worker_agent_report_exception(Web::HTML::WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) =| + did_worker_agent_close(Web::HTML::WorkerAgentOwnerToken owner_token) =| + broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) =| handle_file_return(i32 error, Optional file, i32 request_id) =| diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index f157268bf8..10b08d23d2 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -28,6 +28,7 @@ set(SOURCES Utilities.cpp ViewImplementation.cpp WebContentClient.cpp + WorkerProcessManager.cpp WebUI.cpp WebUI/BookmarksUI.cpp WebUI/ProcessesUI.cpp diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index ec01a5b0d9..a8e376f393 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace WebView { @@ -55,6 +56,7 @@ WebContentClient::WebContentClient(NonnullOwnPtr transport) WebContentClient::~WebContentClient() { + WorkerProcessManager::the().remove_web_content_owner(*this); s_clients.remove(this); } @@ -930,6 +932,7 @@ void WebContentClient::did_post_broadcast_channel_message(u64, Web::HTML::Broadc client.async_broadcast_channel_message(message); return IterationDecision::Continue; }); + WorkerProcessManager::the().broadcast_channel_message_from_web_content(message); } Messages::WebContentClient::DidRequestNewWebViewResponse WebContentClient::did_request_new_web_view(u64 page_id, Web::HTML::ActivateTab activate_tab, Web::HTML::WebViewHints hints, Optional page_index) @@ -1130,17 +1133,19 @@ void WebContentClient::did_present_backing_stores(u64 page_id, i32 front_bitmap_ } } -Messages::WebContentClient::RequestWorkerAgentResponse WebContentClient::request_worker_agent(u64 page_id, Web::Bindings::AgentType worker_type) +Messages::WebContentClient::StartWorkerAgentResponse WebContentClient::start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) { if (auto view = view_for_page_id(page_id); view.has_value()) { - auto request_server_handle = MUST(connect_new_request_server_client()); - auto image_decoder_handle = MUST(connect_new_image_decoder_client()); - auto worker_client = MUST(WebView::launch_web_worker_process(worker_type)); - auto worker_handle = MUST(worker_client->transport().release_for_transfer()); - return { move(worker_handle), move(request_server_handle), move(image_decoder_handle) }; + auto agent_id = WorkerProcessManager::the().start_worker_agent(*this, page_id, move(request)); + return { agent_id }; } - return { IPC::TransportHandle {}, IPC::TransportHandle {}, IPC::TransportHandle {} }; + return { 0 }; +} + +void WebContentClient::close_worker_agent(u64, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) +{ + WorkerProcessManager::the().close_worker_agent(*this, agent_id, owner_token); } Optional WebContentClient::view_for_page_id(u64 page_id, SourceLocation location) diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 1cc9c1c703..1398d8e8f0 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -184,7 +185,8 @@ private: virtual void did_request_paste(u64 page_id) override; virtual void did_change_audio_play_state(u64 page_id, Web::HTML::AudioPlayState) override; virtual void did_update_navigation_buttons_state(u64 page_id, bool back_enabled, bool forward_enabled) override; - virtual Messages::WebContentClient::RequestWorkerAgentResponse request_worker_agent(u64 page_id, Web::Bindings::AgentType worker_type) override; + virtual Messages::WebContentClient::StartWorkerAgentResponse start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) override; + virtual void close_worker_agent(u64 page_id, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) override; Optional view_for_page_id(u64, SourceLocation = SourceLocation::current()); diff --git a/Libraries/LibWebView/WorkerProcessManager.cpp b/Libraries/LibWebView/WorkerProcessManager.cpp new file mode 100644 index 0000000000..a3b12d105b --- /dev/null +++ b/Libraries/LibWebView/WorkerProcessManager.cpp @@ -0,0 +1,429 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebView { + +WorkerProcessManager& WorkerProcessManager::the() +{ + static WorkerProcessManager manager; + return manager; +} + +Web::HTML::WorkerAgentId WorkerProcessManager::start_worker_agent(WebContentClient& owner, u64 page_id, Web::HTML::WorkerAgentStartRequest request) +{ + auto abstract_owner = Owner { + .client = WebContentOwner { + .client = owner, + .page_id = page_id, + }, + .token = request.owner_token, + }; + return start_worker_agent(move(abstract_owner), move(request)); +} + +Web::HTML::WorkerAgentId WorkerProcessManager::start_worker_agent(Web::HTML::WebWorkerClient& owner, Web::HTML::WorkerAgentStartRequest request) +{ + auto abstract_owner = Owner { + .client = WebWorkerOwner { + .client = owner, + }, + .token = request.owner_token, + }; + return start_worker_agent(move(abstract_owner), move(request)); +} + +// https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker +Web::HTML::WorkerAgentId WorkerProcessManager::start_worker_agent(Owner owner, Web::HTML::WorkerAgentStartRequest request) +{ + // 11.1. Let workerGlobalScope be null. + if (request.agent_type == Web::Bindings::AgentType::SharedWorker) { + SharedWorkerKey key { + .storage_key = request.storage_key, + .url = request.url, + .name = request.name, + }; + + // 11.2. For each scope in the list of all SharedWorkerGlobalScope objects: if workerStorageKey + // equals outsideStorageKey, scope's closing flag is false, scope's constructor URL equals + // urlRecord, and scope's name equals options["name"], then set workerGlobalScope to scope + // and break. + if (auto existing_agent_id = m_shared_workers.get(key); existing_agent_id.has_value()) { + auto maybe_agent = m_agents.find(*existing_agent_id); + if (maybe_agent != m_agents.end()) { + auto& agent = maybe_agent->value; + if (!agent.closing) { + // FIXME: 11.3. If workerGlobalScope is not null, but the user agent has been + // configured to disallow communication between the worker represented + // by the workerGlobalScope and the scripts whose settings object is + // outsideSettings, then set workerGlobalScope to null. + + // 11.4. If workerGlobalScope is not null, and any of the following are true: + // workerGlobalScope's type is not equal to options["type"]; + // workerGlobalScope's credentials is not equal to options["credentials"]; or + // workerGlobalScope's extended lifetime is not equal to + // options["extendedLifetime"], then queue a global task on the DOM + // manipulation task source given worker's relevant global object to fire an + // event named error at worker and abort these steps. + // 11.5.3. If workerIsSecureContext is not callerIsSecureContext, queue a global task + // on the DOM manipulation task source given worker's relevant global object + // to fire an event named error at worker and abort these steps. + // AD-HOC: Error firing is routed back over IPC via notify_worker_script_load_failure; + // the spec's queue-global-task happens inside WorkerAgentParent in the + // requesting process. + // FIXME: The extendedLifetime comparison becomes observable once SharedWorkerOptions is implemented. + if (agent.worker_type != request.type + || agent.credentials != request.credentials + || agent.extended_lifetime != request.extended_lifetime + || (agent.worker_is_secure_context.has_value() && *agent.worker_is_secure_context != request.caller_is_secure_context)) { + notify_worker_script_load_failure(owner); + return 0; + } + + // 11.5.8. Append the relevant owner to add given outsideSettings to + // workerGlobalScope's owner set. + // AD-HOC: The browser-side mirror lives in `agent.owners`; the worker-process + // owner_set() is appended inside connect_shared_worker_impl (which also + // handles steps 11.5.5-11.5.7). + agent.owners.append(owner); + agent.client->async_connect_shared_worker(move(request.outside_port), request.outside_settings); + notify_worker_script_load_success(owner); + return agent.id; + } + } + + m_shared_workers.remove(key); + } + } + + // 11.6. Otherwise, in parallel, run a worker given worker, urlRecord, outsideSettings, outsidePort, + // and options. + // AD-HOC: For DedicatedWorker there is no shared worker manager step; we always launch a fresh + // worker process here. + auto agent_id = ++m_next_agent_id; + auto client = MUST(launch_web_worker_process(request.agent_type)); + + client->on_worker_close = [this, agent_id] { + worker_did_close(agent_id); + }; + client->on_worker_script_load_success = [this, agent_id](bool worker_is_secure_context) { + worker_did_finish_loading_script(agent_id, worker_is_secure_context); + }; + client->on_worker_script_load_failure = [this, agent_id] { + worker_did_fail_loading_script(agent_id); + }; + client->on_worker_exception = [this, agent_id](String message, String filename, u32 lineno, u32 colno) { + worker_did_report_exception(agent_id, move(message), move(filename), lineno, colno); + }; + client->on_worker_died = [this, agent_id] { + worker_did_die(agent_id); + }; + client->on_request_cookie = [](URL::URL const& url, HTTP::Cookie::Source source) { + HTTP::Cookie::VersionedCookie cookie; + cookie.cookie = Application::cookie_jar().get_cookie(url, source); + return cookie; + }; + client->on_request_file = [this, agent_id](ByteString path, i32 request_id) { + worker_did_request_file(agent_id, move(path), request_id); + }; + client->on_post_broadcast_channel_message = [this, agent_id](Web::HTML::BroadcastChannelMessage message) { + worker_did_post_broadcast_channel_message(agent_id, move(message)); + }; + client->on_start_worker_agent = [this, client = client.ptr()](Web::HTML::WorkerAgentStartRequest request) { + return start_worker_agent(*client, move(request)); + }; + client->on_close_worker_agent = [this, client = client.ptr()](Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) { + close_worker_agent(*client, agent_id, owner_token); + }; + + auto request_server_handle = MUST(connect_new_request_server_client()); + auto image_decoder_handle = MUST(connect_new_image_decoder_client()); + client->async_connect_to_request_server(move(request_server_handle)); + client->async_connect_to_image_decoder(move(image_decoder_handle)); + + Vector owners; + owners.append(owner); + + // AD-HOC: Seed worker_is_secure_context with the caller's value so reuse requests arriving before + // the worker finishes loading still get a mismatch check. + // worker_did_finish_loading_script overwrites this with the worker's actual value (which + // inherits from outside settings, so should match). + WorkerAgent agent { + .id = agent_id, + .client = client, + .agent_type = request.agent_type, + .worker_type = request.type, + .credentials = request.credentials, + .extended_lifetime = request.extended_lifetime, + .worker_is_secure_context = request.caller_is_secure_context, + .shared_worker_key = {}, + .owners = move(owners), + }; + + if (request.agent_type == Web::Bindings::AgentType::SharedWorker) { + agent.shared_worker_key = SharedWorkerKey { + .storage_key = request.storage_key, + .url = request.url, + .name = request.name, + }; + m_shared_workers.set(*agent.shared_worker_key, agent_id); + } + + m_agents.set(agent_id, move(agent)); + client->async_start_worker(request.url, request.type, request.credentials, request.name, move(request.outside_port), request.outside_settings, request.agent_type); + + return agent_id; +} + +void WorkerProcessManager::close_worker_agent(WebContentClient& client, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) +{ + Owner identity { + .client = WebContentOwner { .client = client }, + .token = owner_token, + }; + remove_owner(agent_id, identity); +} + +void WorkerProcessManager::close_worker_agent(Web::HTML::WebWorkerClient& client, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) +{ + Owner identity { + .client = WebWorkerOwner { .client = client }, + .token = owner_token, + }; + remove_owner(agent_id, identity); +} + +void WorkerProcessManager::remove_web_content_owner(WebContentClient& client) +{ + Vector agents_to_close; + for (auto& entry : m_agents) { + auto& agent = entry.value; + agent.owners.remove_all_matching([&](Owner const& owner) { + auto const* web_content_owner = owner.client.get_pointer(); + return web_content_owner && web_content_owner->client.ptr() == &client; + }); + if (agent.owners.is_empty()) + agents_to_close.append(agent.id); + } + + for (auto agent_id : agents_to_close) + remove_agent(agent_id); +} + +void WorkerProcessManager::broadcast_channel_message_from_web_content(Web::HTML::BroadcastChannelMessage const& message) +{ + for (auto& entry : m_agents) { + auto& agent = entry.value; + if (agent.client->pid() == message.source_process_id) + continue; + agent.client->async_broadcast_channel_message(message); + } +} + +void WorkerProcessManager::notify_worker_script_load_success(Owner const& owner) +{ + owner.client.visit( + [&](WebContentOwner const& web_content_owner) { + if (web_content_owner.client) + web_content_owner.client->async_did_worker_agent_finish_loading_script(owner.token); + }, + [&](WebWorkerOwner const& web_worker_owner) { + web_worker_owner.client->async_did_worker_agent_finish_loading_script(owner.token); + }); +} + +void WorkerProcessManager::notify_worker_script_load_failure(Owner const& owner) +{ + owner.client.visit( + [&](WebContentOwner const& web_content_owner) { + if (web_content_owner.client) + web_content_owner.client->async_did_worker_agent_fail_loading_script(owner.token); + }, + [&](WebWorkerOwner const& web_worker_owner) { + web_worker_owner.client->async_did_worker_agent_fail_loading_script(owner.token); + }); +} + +void WorkerProcessManager::notify_worker_exception(Owner const& owner, String const& message, String const& filename, u32 lineno, u32 colno) +{ + owner.client.visit( + [&](WebContentOwner const& web_content_owner) { + if (web_content_owner.client) + web_content_owner.client->async_did_worker_agent_report_exception(owner.token, message, filename, lineno, colno); + }, + [&](WebWorkerOwner const& web_worker_owner) { + web_worker_owner.client->async_did_worker_agent_report_exception(owner.token, message, filename, lineno, colno); + }); +} + +void WorkerProcessManager::notify_worker_close(Owner const& owner) +{ + owner.client.visit( + [&](WebContentOwner const& web_content_owner) { + if (web_content_owner.client) + web_content_owner.client->async_did_worker_agent_close(owner.token); + }, + [&](WebWorkerOwner const& web_worker_owner) { + web_worker_owner.client->async_did_worker_agent_close(owner.token); + }); +} + +void WorkerProcessManager::worker_did_finish_loading_script(Web::HTML::WorkerAgentId agent_id, bool worker_is_secure_context) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto& agent = maybe_agent->value; + agent.worker_is_secure_context = worker_is_secure_context; + + for (auto const& owner : agent.owners) + notify_worker_script_load_success(owner); +} + +void WorkerProcessManager::worker_did_fail_loading_script(Web::HTML::WorkerAgentId agent_id) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto& agent = maybe_agent->value; + if (agent.closing) + return; + + agent.closing = true; + auto owners = agent.owners; + for (auto const& owner : owners) + notify_worker_script_load_failure(owner); + + Core::deferred_invoke([this, agent_id] { + remove_agent(agent_id); + }); +} + +void WorkerProcessManager::worker_did_report_exception(Web::HTML::WorkerAgentId agent_id, String message, String filename, u32 lineno, u32 colno) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + for (auto const& owner : maybe_agent->value.owners) + notify_worker_exception(owner, message, filename, lineno, colno); +} + +void WorkerProcessManager::worker_did_close(Web::HTML::WorkerAgentId agent_id) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto& agent = maybe_agent->value; + if (agent.closing) + return; + + agent.closing = true; + auto owners = agent.owners; + for (auto const& owner : owners) + notify_worker_close(owner); + + Core::deferred_invoke([this, agent_id] { + remove_agent(agent_id); + }); +} + +void WorkerProcessManager::worker_did_die(Web::HTML::WorkerAgentId agent_id) +{ + worker_did_close(agent_id); +} + +void WorkerProcessManager::worker_did_request_file(Web::HTML::WorkerAgentId agent_id, ByteString path, i32 request_id) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto file = Core::File::open(path, Core::File::OpenMode::Read); + if (file.is_error()) + maybe_agent->value.client->async_handle_file_return(file.error().code(), {}, request_id); + else + maybe_agent->value.client->async_handle_file_return(0, IPC::File::adopt_file(file.release_value()), request_id); +} + +void WorkerProcessManager::worker_did_post_broadcast_channel_message(Web::HTML::WorkerAgentId agent_id, Web::HTML::BroadcastChannelMessage message) +{ + WebContentClient::for_each_client([&](auto& client) { + if (client.pid() == message.source_process_id) + return IterationDecision::Continue; + client.async_broadcast_channel_message(message); + return IterationDecision::Continue; + }); + + for (auto& entry : m_agents) { + if (entry.key == agent_id) + continue; + auto& agent = entry.value; + if (agent.client->pid() == message.source_process_id) + continue; + agent.client->async_broadcast_channel_message(message); + } +} + +void WorkerProcessManager::remove_agent(Web::HTML::WorkerAgentId agent_id) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto agent = move(maybe_agent->value); + m_agents.remove(agent_id); + + if (agent.shared_worker_key.has_value()) + m_shared_workers.remove(*agent.shared_worker_key); + + agent.closing = true; + if (agent.client->is_open()) + agent.client->async_close_worker(); +} + +void WorkerProcessManager::remove_owner(Web::HTML::WorkerAgentId agent_id, Owner const& identity) +{ + auto maybe_agent = m_agents.find(agent_id); + if (maybe_agent == m_agents.end()) + return; + + auto& agent = maybe_agent->value; + auto agent_owned_by_specified_owner = agent.owners.remove_all_matching([&](Owner const& owner) { + if (owner.token != identity.token) + return false; + + if (auto const* incoming = identity.client.get_pointer()) { + auto const* candidate = owner.client.get_pointer(); + return candidate && candidate->client.ptr() == incoming->client.ptr(); + } + if (auto const* incoming = identity.client.get_pointer()) { + auto const* candidate = owner.client.get_pointer(); + return candidate && candidate->client.ptr() == incoming->client.ptr(); + } + return false; + }); + + if (!agent_owned_by_specified_owner) + return; + + if (agent.agent_type == Web::Bindings::AgentType::DedicatedWorker || agent.owners.is_empty()) + remove_agent(agent_id); +} + +} diff --git a/Libraries/LibWebView/WorkerProcessManager.h b/Libraries/LibWebView/WorkerProcessManager.h new file mode 100644 index 0000000000..8115b46abd --- /dev/null +++ b/Libraries/LibWebView/WorkerProcessManager.h @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +class WebWorkerClient; + +} + +namespace WebView { + +class WorkerProcessManager { +public: + static WorkerProcessManager& the(); + + struct SharedWorkerKey { + Web::StorageAPI::StorageKey storage_key; + URL::URL url; + String name; + + bool operator==(SharedWorkerKey const&) const = default; + }; + + Web::HTML::WorkerAgentId start_worker_agent(WebContentClient&, u64 page_id, Web::HTML::WorkerAgentStartRequest); + Web::HTML::WorkerAgentId start_worker_agent(Web::HTML::WebWorkerClient&, Web::HTML::WorkerAgentStartRequest); + + void close_worker_agent(WebContentClient&, Web::HTML::WorkerAgentId, Web::HTML::WorkerAgentOwnerToken); + void close_worker_agent(Web::HTML::WebWorkerClient&, Web::HTML::WorkerAgentId, Web::HTML::WorkerAgentOwnerToken); + void remove_web_content_owner(WebContentClient&); + + void broadcast_channel_message_from_web_content(Web::HTML::BroadcastChannelMessage const&); + +private: + WorkerProcessManager() = default; + + struct WebContentOwner { + WeakPtr client; + u64 page_id { 0 }; + }; + + struct WebWorkerOwner { + NonnullRefPtr client; + }; + + struct Owner { + Variant client; + Web::HTML::WorkerAgentOwnerToken token { 0 }; + }; + + Web::HTML::WorkerAgentId start_worker_agent(Owner, Web::HTML::WorkerAgentStartRequest); + + void notify_worker_script_load_success(Owner const&); + void notify_worker_script_load_failure(Owner const&); + void notify_worker_exception(Owner const&, String const& message, String const& filename, u32 lineno, u32 colno); + void notify_worker_close(Owner const&); + + void worker_did_finish_loading_script(Web::HTML::WorkerAgentId, bool worker_is_secure_context); + void worker_did_fail_loading_script(Web::HTML::WorkerAgentId); + void worker_did_report_exception(Web::HTML::WorkerAgentId, String message, String filename, u32 lineno, u32 colno); + void worker_did_close(Web::HTML::WorkerAgentId); + void worker_did_die(Web::HTML::WorkerAgentId); + void worker_did_request_file(Web::HTML::WorkerAgentId, ByteString path, i32 request_id); + void worker_did_post_broadcast_channel_message(Web::HTML::WorkerAgentId, Web::HTML::BroadcastChannelMessage); + + void remove_agent(Web::HTML::WorkerAgentId); + void remove_owner(Web::HTML::WorkerAgentId, Owner const& identity); + + struct WorkerAgent { + Web::HTML::WorkerAgentId id { 0 }; + NonnullRefPtr client; + Web::Bindings::AgentType agent_type { Web::Bindings::AgentType::DedicatedWorker }; + Web::Bindings::WorkerType worker_type { Web::Bindings::WorkerType::Classic }; + Web::Bindings::RequestCredentials credentials { Web::Bindings::RequestCredentials::SameOrigin }; + bool extended_lifetime { false }; + Optional worker_is_secure_context; + bool closing { false }; + Optional shared_worker_key; + Vector owners; + }; + + Web::HTML::WorkerAgentId m_next_agent_id { 0 }; + HashMap m_agents; + HashMap m_shared_workers; +}; + +} + +namespace AK { + +template<> +struct Traits : public DefaultTraits { + static unsigned hash(WebView::WorkerProcessManager::SharedWorkerKey const& key) + { + return pair_int_hash(pair_int_hash(Traits::hash(key.storage_key), Traits::hash(key.url)), key.name.hash()); + } +}; + +} diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index d0e52ede0d..50f7ee0e63 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +59,6 @@ #include #include #include -#include #include #include #include @@ -1467,12 +1467,26 @@ void ConnectionFromClient::cookies_changed(u64 page_id, Vector #include #include +#include #include #include #include @@ -177,6 +178,10 @@ private: virtual void set_document_cookie_version_index(u64 page_id, i64 document_id, Core::SharedVersionIndex document_index) override; virtual void cookies_changed(u64 page_id, Vector) override; virtual void broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) override; + virtual void did_worker_agent_finish_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) override; + virtual void did_worker_agent_fail_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) override; + virtual void did_worker_agent_report_exception(Web::HTML::WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) override; + virtual void did_worker_agent_close(Web::HTML::WorkerAgentOwnerToken owner_token) override; virtual void request_close(u64 page_id) override; diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index 20892300cd..1a81b57931 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -775,15 +775,20 @@ void PageClient::page_did_change_audio_play_state(Web::HTML::AudioPlayState play client().async_did_change_audio_play_state(m_id, play_state); } -Web::PageClient::WorkerAgentResponse PageClient::request_worker_agent(Web::Bindings::AgentType type) +Web::HTML::WorkerAgentId PageClient::start_worker_agent(Web::HTML::WorkerAgentStartRequest&& request) { - auto response = client().send_sync_but_allow_failure(m_id, type); + auto response = client().send_sync_but_allow_failure(m_id, move(request)); if (!response) { - dbgln("WebContent client disconnected during RequestWorkerAgent. Exiting peacefully."); + dbgln("WebContent client disconnected during StartWorkerAgent. Exiting peacefully."); exit(0); } - return { response->take_handle(), response->take_request_server_handle(), response->take_image_decoder_handle() }; + return response->agent_id(); +} + +void PageClient::close_worker_agent(Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) +{ + client().async_close_worker_agent(m_id, agent_id, owner_token); } void PageClient::page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr, GC::Ptr, Optional const& attribute_name) diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index 18f8136514..e17acdb93e 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -197,7 +197,8 @@ private: virtual void page_did_request_clipboard_entries(u64 request_id) override; virtual void page_did_request_paste() override; virtual void page_did_change_audio_play_state(Web::HTML::AudioPlayState) override; - virtual WorkerAgentResponse request_worker_agent(Web::Bindings::AgentType) override; + virtual Web::HTML::WorkerAgentId start_worker_agent(Web::HTML::WorkerAgentStartRequest&&) override; + virtual void close_worker_agent(Web::HTML::WorkerAgentId, Web::HTML::WorkerAgentOwnerToken) override; virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr previous_sibling, GC::Ptr next_sibling, Optional const& attribute_name) override; virtual void page_did_take_screenshot(Gfx::ShareableBitmap const& screenshot) override; virtual void received_message_from_web_ui(String const& name, JS::Value data) override; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index fc3066cb3f..59606c136a 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -142,5 +143,6 @@ endpoint WebContentClient did_find_in_page(u64 page_id, size_t current_match_index, Optional total_match_count) =| - request_worker_agent(u64 page_id, Web::Bindings::AgentType worker_type) => (IPC::TransportHandle handle, IPC::TransportHandle request_server_handle, IPC::TransportHandle image_decoder_handle) // FIXME: Add required attributes to select a SharedWorker Agent + start_worker_agent(u64 page_id, Web::HTML::WorkerAgentStartRequest request) => (Web::HTML::WorkerAgentId agent_id) + close_worker_agent(u64 page_id, Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) =| } diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 0f6c081e09..9f4b7fc9ce 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,11 @@ endpoint WebContentServer cookies_changed(u64 page_id, Vector cookies) =| broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) =| + did_worker_agent_finish_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) =| + did_worker_agent_fail_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) =| + did_worker_agent_report_exception(Web::HTML::WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) =| + did_worker_agent_close(Web::HTML::WorkerAgentOwnerToken owner_token) =| + request_close(u64 page_id) =| exit_fullscreen(u64 page_id) =| diff --git a/Services/WebWorker/ConnectionFromClient.cpp b/Services/WebWorker/ConnectionFromClient.cpp index fa2c5de49f..85e56c39ff 100644 --- a/Services/WebWorker/ConnectionFromClient.cpp +++ b/Services/WebWorker/ConnectionFromClient.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include #include @@ -44,19 +44,11 @@ void ConnectionFromClient::die() void ConnectionFromClient::request_file(Web::FileRequest request) { - // FIXME: Route this to FSAS or browser process as appropriate instead of allowing - // the WebWorker process filesystem access - auto path = request.path(); auto request_id = ++last_id; + auto path = request.path(); m_requested_files.set(request_id, move(request)); - - auto file = Core::File::open(path, Core::File::OpenMode::Read); - - if (file.is_error()) - handle_file_return(file.error().code(), {}, request_id); - else - handle_file_return(0, IPC::File::adopt_file(file.release_value()), request_id); + async_did_request_file(path, request_id); } ConnectionFromClient::ConnectionFromClient(NonnullOwnPtr transport) @@ -89,6 +81,13 @@ void ConnectionFromClient::start_worker(URL::URL url, Web::Bindings::WorkerType m_worker_host->run(page(), move(implicit_port), outside_settings, credentials, is_shared); } +void ConnectionFromClient::connect_shared_worker(Web::HTML::TransferDataEncoder message_port, Web::HTML::SerializedEnvironmentSettingsObject outside_settings) +{ + if (!m_worker_host) + return; + m_worker_host->connect_shared_worker(move(message_port), move(outside_settings)); +} + void ConnectionFromClient::handle_file_return(i32 error, Optional file, i32 request_id) { auto file_request = m_requested_files.take(request_id); @@ -99,15 +98,29 @@ void ConnectionFromClient::handle_file_return(i32 error, Optional fil file_request.value().on_file_request_finish(error != 0 ? Error::from_errno(error) : ErrorOr { file->take_fd() }); } +void ConnectionFromClient::did_worker_agent_finish_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) +{ + Web::HTML::WorkerAgentParent::did_finish_loading_worker_script(owner_token); +} + +void ConnectionFromClient::did_worker_agent_fail_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) +{ + Web::HTML::WorkerAgentParent::did_fail_loading_worker_script(owner_token); +} + +void ConnectionFromClient::did_worker_agent_report_exception(Web::HTML::WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) +{ + Web::HTML::WorkerAgentParent::did_report_worker_exception(owner_token, move(message), move(filename), lineno, colno); +} + +void ConnectionFromClient::did_worker_agent_close(Web::HTML::WorkerAgentOwnerToken owner_token) +{ + Web::HTML::WorkerAgentParent::did_close_worker(owner_token); +} + void ConnectionFromClient::broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) { Web::HTML::BroadcastChannel::deliver_message_locally(message); - Web::HTML::WebWorkerClient::for_each_client([&](auto& client) { - if (client.pid() == message.source_process_id) - return IterationDecision::Continue; - client.async_broadcast_channel_message(message); - return IterationDecision::Continue; - }); } } diff --git a/Services/WebWorker/ConnectionFromClient.h b/Services/WebWorker/ConnectionFromClient.h index 6dc12cb952..80fa5263e7 100644 --- a/Services/WebWorker/ConnectionFromClient.h +++ b/Services/WebWorker/ConnectionFromClient.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -48,7 +49,12 @@ private: virtual void connect_to_request_server(IPC::TransportHandle handle) override; virtual void connect_to_image_decoder(IPC::TransportHandle handle) override; virtual void start_worker(URL::URL url, Web::Bindings::WorkerType type, Web::Bindings::RequestCredentials credentials, String name, Web::HTML::TransferDataEncoder, Web::HTML::SerializedEnvironmentSettingsObject, Web::Bindings::AgentType) override; + virtual void connect_shared_worker(Web::HTML::TransferDataEncoder, Web::HTML::SerializedEnvironmentSettingsObject) override; virtual void handle_file_return(i32 error, Optional file, i32 request_id) override; + virtual void did_worker_agent_finish_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) override; + virtual void did_worker_agent_fail_loading_script(Web::HTML::WorkerAgentOwnerToken owner_token) override; + virtual void did_worker_agent_report_exception(Web::HTML::WorkerAgentOwnerToken owner_token, String message, String filename, u32 lineno, u32 colno) override; + virtual void did_worker_agent_close(Web::HTML::WorkerAgentOwnerToken owner_token) override; virtual void broadcast_channel_message(Web::HTML::BroadcastChannelMessage message) override; GC::Root m_page_host; diff --git a/Services/WebWorker/PageHost.cpp b/Services/WebWorker/PageHost.cpp index 5ba8629930..99e6cb7d58 100644 --- a/Services/WebWorker/PageHost.cpp +++ b/Services/WebWorker/PageHost.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -107,15 +108,19 @@ void PageHost::request_file(Web::FileRequest request) m_client.request_file(move(request)); } -Web::PageClient::WorkerAgentResponse PageHost::request_worker_agent(Web::Bindings::AgentType worker_type) +Web::HTML::WorkerAgentId PageHost::start_worker_agent(Web::HTML::WorkerAgentStartRequest&& request) { - auto response = m_client.request_worker_agent(worker_type); - return { response.take_handle(), response.take_request_server_handle(), response.take_image_decoder_handle() }; + return m_client.start_worker_agent(move(request)); } -void PageHost::did_finish_loading_worker_script() +void PageHost::close_worker_agent(Web::HTML::WorkerAgentId agent_id, Web::HTML::WorkerAgentOwnerToken owner_token) { - m_client.async_did_finish_loading_worker_script(); + m_client.async_close_worker_agent(agent_id, owner_token); +} + +void PageHost::did_finish_loading_worker_script(bool worker_is_secure_context) +{ + m_client.async_did_finish_loading_worker_script(worker_is_secure_context); } void PageHost::did_fail_loading_worker_script() diff --git a/Services/WebWorker/PageHost.h b/Services/WebWorker/PageHost.h index e0c38e32a5..65dba01982 100644 --- a/Services/WebWorker/PageHost.h +++ b/Services/WebWorker/PageHost.h @@ -40,13 +40,14 @@ public: virtual void page_did_report_worker_exception(String const& message, String const& filename, u32 lineno, u32 colno) override; virtual void page_did_post_broadcast_channel_message(Web::HTML::BroadcastChannelMessage const& message) override; virtual void request_file(Web::FileRequest) override; - virtual WorkerAgentResponse request_worker_agent(Web::Bindings::AgentType) override; + virtual Web::HTML::WorkerAgentId start_worker_agent(Web::HTML::WorkerAgentStartRequest&&) override; + virtual void close_worker_agent(Web::HTML::WorkerAgentId, Web::HTML::WorkerAgentOwnerToken) override; virtual Web::DisplayListPlayerType display_list_player_type() const override { VERIFY_NOT_REACHED(); } virtual bool is_headless() const override { VERIFY_NOT_REACHED(); } virtual Queue& input_event_queue() override { VERIFY_NOT_REACHED(); } virtual void report_finished_handling_input_event([[maybe_unused]] u64 page_id, [[maybe_unused]] Web::EventResult event_was_handled) override { VERIFY_NOT_REACHED(); } virtual void request_frame() override { VERIFY_NOT_REACHED(); } - void did_finish_loading_worker_script(); + void did_finish_loading_worker_script(bool worker_is_secure_context); void did_fail_loading_worker_script(); private: diff --git a/Services/WebWorker/WorkerHost.cpp b/Services/WebWorker/WorkerHost.cpp index 7b93ac7967..859e520687 100644 --- a/Services/WebWorker/WorkerHost.cpp +++ b/Services/WebWorker/WorkerHost.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +53,8 @@ static Web::HTML::WorkerGlobalScope::Owner relevant_owner_to_add(Web::HTML::Seri // https://html.spec.whatwg.org/multipage/workers.html#run-a-worker void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject const& outside_settings_snapshot, Web::Bindings::RequestCredentials credentials, bool is_shared) { + m_is_shared = is_shared; + // 1. Let is shared be true if worker is a SharedWorker object, and false otherwise. // 2. Let owner be the relevant owner to add given outside settings. auto owner = relevant_owner_to_add(outside_settings_snapshot); @@ -73,6 +77,7 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes // 6. Let worker global scope be the global object of realm execution context's Realm component. // NOTE: This is the DedicatedWorkerGlobalScope or SharedWorkerGlobalScope object created in the previous step. GC::Ref worker_global_scope = as(realm_execution_context->realm->global_object()); + m_worker_global_scope = worker_global_scope; // AD-HOC: The spec assumes when setting up the worker environment settings object that the URL is already set on // the worker global scope. This is not the case. This URL is only known after performing the fetch, and in @@ -92,6 +97,7 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes // 7. Set up a worker environment settings object with realm execution context, outside settings, and // unsafeWorkerCreationTime, and let inside settings be the result. auto inside_settings = Web::HTML::WorkerEnvironmentSettingsObject::setup(page, move(realm_execution_context), outside_settings_snapshot, unsafe_worker_creation_time); + m_inside_settings = inside_settings; // AD-HOC: Create a console object for the worker. auto& console_object = *inside_settings->realm().intrinsics().console_object(); @@ -201,7 +207,8 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes auto perform_fetch = Web::HTML::create_perform_the_fetch_hook(inside_settings->heap(), move(perform_fetch_function)); // In both cases, let onComplete given script be the following steps: - auto on_complete_function = [page, inside_settings, worker_global_scope, message_port_data = move(message_port_data), url = m_url, is_shared](GC::Ptr script) mutable { + RefPtr protected_this { *this }; + auto on_complete_function = [protected_this, page, inside_settings, worker_global_scope, message_port_data = move(message_port_data), outside_settings_snapshot, url = m_url, is_shared](GC::Ptr script) mutable { auto& realm = inside_settings->realm(); // 1. If script is null or if script's error to rethrow is non-null, then: @@ -220,20 +227,25 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes // FIXME: 2. Associate worker with worker global scope. // What does this even mean? - // 3. Let inside port be a new MessagePort object in inside settings's realm. - auto inside_port = Web::HTML::MessagePort::create(realm); + GC::Ptr inside_port; + // 3. Let inside port be a new MessagePort object in inside settings's realm. // 4. If is shared is false, then: + // 5. Entangle outside port and inside port. + // AD-HOC: For shared workers we defer the inside port creation (step 3) and entanglement + // (step 5) to connect_shared_worker_impl. Each connecting page gets its own pair + // since the connect event fires once per owner, not once at worker startup. if (!is_shared) { + inside_port = Web::HTML::MessagePort::create(realm); + // FIXME: 1. Set inside port's message event target to worker global scope. // 2. Set worker global scope's inside port to inside port. - worker_global_scope->set_internal_port(inside_port); - } + worker_global_scope->set_internal_port(*inside_port); - // 5. Entangle outside port and inside port. - Web::HTML::TransferDataDecoder decoder { move(message_port_data) }; - MUST(inside_port->transfer_receiving_steps(decoder)); + Web::HTML::TransferDataDecoder decoder { move(message_port_data) }; + MUST(inside_port->transfer_receiving_steps(decoder)); + } // 6. Create a new WorkerLocation object and associate it with worker global scope. worker_global_scope->set_location(realm.create(*worker_global_scope)); @@ -257,7 +269,7 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes else (void)as(*script).run(); - as(page->client()).did_finish_loading_worker_script(); + as(page->client()).did_finish_loading_worker_script(Web::HTML::is_secure_context(*inside_settings)); // FIXME: 11. Enable outside port's port message queue. @@ -271,19 +283,9 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes // with the data attribute initialized to the empty string, the ports attribute initialized // to a new frozen array containing inside port, and the source attribute initialized to inside port. if (is_shared) { - Web::HTML::queue_global_task(Web::HTML::Task::Source::DOMManipulation, *worker_global_scope, GC::create_function(realm.heap(), [worker_global_scope, inside_port] { - auto& realm = worker_global_scope->realm(); - auto& vm = realm.vm(); - Web::HTML::TemporaryExecutionContext const context(realm); - - Web::Bindings::MessageEventInit event_init; - event_init.data = GC::Ref { vm.empty_string() }; - event_init.source = Web::HTML::NullableMessageEventSource { inside_port }; - event_init.ports.append(inside_port); - - auto message_event = Web::HTML::MessageEvent::create(realm, Web::HTML::EventNames::connect, event_init); - worker_global_scope->dispatch_event(message_event); - })); + protected_this->m_accepting_shared_worker_connections = true; + protected_this->connect_shared_worker_impl(move(message_port_data), outside_settings_snapshot, ShouldAppendOwner::No); + protected_this->flush_pending_shared_worker_connections(); } // FIXME: 14. Enable the client message queue of the ServiceWorkerContainer object whose associated service @@ -324,4 +326,65 @@ void WorkerHost::run(GC::Ref page, Web::HTML::TransferDataEncoder mes } } +void WorkerHost::connect_shared_worker(Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject outside_settings) +{ + if (!m_is_shared || !m_accepting_shared_worker_connections) { + m_pending_shared_worker_connections.append(PendingSharedWorkerConnection { + .message_port_data = move(message_port_data), + .outside_settings = move(outside_settings), + }); + return; + } + + connect_shared_worker_impl(move(message_port_data), outside_settings, ShouldAppendOwner::Yes); +} + +// https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker +void WorkerHost::connect_shared_worker_impl(Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject const& outside_settings, ShouldAppendOwner should_append_owner) +{ + VERIFY(m_worker_global_scope); + VERIFY(m_inside_settings); + + auto& realm = m_inside_settings->realm(); + + // 11.5.5. Let insidePort be a new MessagePort in insideSettings's realm. + auto inside_port = Web::HTML::MessagePort::create(realm); + + // 11.5.6. Entangle outsidePort and insidePort. + Web::HTML::TransferDataDecoder decoder { move(message_port_data) }; + MUST(inside_port->transfer_receiving_steps(decoder)); + + // 11.5.7. Queue a global task on the DOM manipulation task source given workerGlobalScope to + // fire an event named connect at workerGlobalScope, using MessageEvent, with the data + // attribute initialized to the empty string, the ports attribute initialized to a new + // frozen array containing only insidePort, and the source attribute initialized to + // insidePort. + Web::HTML::queue_global_task(Web::HTML::Task::Source::DOMManipulation, *m_worker_global_scope, GC::create_function(realm.heap(), [worker_global_scope = GC::Root { *m_worker_global_scope }, inside_port] { + auto& realm = worker_global_scope->realm(); + auto& vm = realm.vm(); + Web::HTML::TemporaryExecutionContext const context(realm); + + Web::Bindings::MessageEventInit event_init {}; + event_init.data = GC::Ref { vm.empty_string() }; + event_init.ports.append(inside_port); + event_init.source = Web::HTML::NullableMessageEventSource { inside_port }; + + auto message_event = Web::HTML::MessageEvent::create(realm, Web::HTML::EventNames::connect, event_init); + worker_global_scope->dispatch_event(message_event); + })); + + // 11.5.8. Append the relevant owner to add given outsideSettings to workerGlobalScope's owner set. + if (should_append_owner == ShouldAppendOwner::Yes) { + auto owner = relevant_owner_to_add(outside_settings); + m_worker_global_scope->owner_set().append(owner); + } +} + +void WorkerHost::flush_pending_shared_worker_connections() +{ + auto pending_connections = move(m_pending_shared_worker_connections); + for (auto& connection : pending_connections) + connect_shared_worker_impl(move(connection.message_port_data), connection.outside_settings, ShouldAppendOwner::Yes); +} + } diff --git a/Services/WebWorker/WorkerHost.h b/Services/WebWorker/WorkerHost.h index b5b16dc91c..54d9973dc1 100644 --- a/Services/WebWorker/WorkerHost.h +++ b/Services/WebWorker/WorkerHost.h @@ -7,12 +7,15 @@ #pragma once #include +#include #include #include #include #include #include +#include #include +#include namespace WebWorker { @@ -22,13 +25,33 @@ public: ~WorkerHost(); void run(GC::Ref, Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject const&, Web::Bindings::RequestCredentials, bool is_shared); + void connect_shared_worker(Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject); private: + struct PendingSharedWorkerConnection { + Web::HTML::TransferDataEncoder message_port_data; + Web::HTML::SerializedEnvironmentSettingsObject outside_settings; + }; + + enum class ShouldAppendOwner { + No, + Yes, + }; + + void connect_shared_worker_impl(Web::HTML::TransferDataEncoder message_port_data, Web::HTML::SerializedEnvironmentSettingsObject const&, ShouldAppendOwner); + void flush_pending_shared_worker_connections(); + GC::Root m_console; + GC::Root m_worker_global_scope; + GC::Root m_inside_settings; URL::URL m_url; Web::Bindings::WorkerType m_type; String m_name; + bool m_is_shared { false }; + bool m_accepting_shared_worker_connections { false }; + // WorkerHost is only touched on the WebWorker main thread. + Vector m_pending_shared_worker_connections; }; } diff --git a/Tests/LibWeb/Text/expected/Worker/SharedWorker-close-one-owner.txt b/Tests/LibWeb/Text/expected/Worker/SharedWorker-close-one-owner.txt new file mode 100644 index 0000000000..0fa872cd0b --- /dev/null +++ b/Tests/LibWeb/Text/expected/Worker/SharedWorker-close-one-owner.txt @@ -0,0 +1,2 @@ +worker2: connect 2 +worker2: echo 2: after-gc diff --git a/Tests/LibWeb/Text/expected/Worker/SharedWorker-reuse.txt b/Tests/LibWeb/Text/expected/Worker/SharedWorker-reuse.txt new file mode 100644 index 0000000000..55117a1984 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Worker/SharedWorker-reuse.txt @@ -0,0 +1,4 @@ +worker1: connect 1 +worker2: connect 2 +worker1: echo 1: one +worker2: echo 2: two diff --git a/Tests/LibWeb/Text/expected/Worker/SharedWorker-type-mismatch.txt b/Tests/LibWeb/Text/expected/Worker/SharedWorker-type-mismatch.txt new file mode 100644 index 0000000000..2f6fba5997 --- /dev/null +++ b/Tests/LibWeb/Text/expected/Worker/SharedWorker-type-mismatch.txt @@ -0,0 +1,2 @@ +worker1: connect 1 +worker2: error diff --git a/Tests/LibWeb/Text/input/Worker/SharedWorker-close-one-owner.html b/Tests/LibWeb/Text/input/Worker/SharedWorker-close-one-owner.html new file mode 100644 index 0000000000..8e2d7576b1 --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/SharedWorker-close-one-owner.html @@ -0,0 +1,59 @@ + + + diff --git a/Tests/LibWeb/Text/input/Worker/SharedWorker-reuse.html b/Tests/LibWeb/Text/input/Worker/SharedWorker-reuse.html new file mode 100644 index 0000000000..b97e2edead --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/SharedWorker-reuse.html @@ -0,0 +1,46 @@ + + + diff --git a/Tests/LibWeb/Text/input/Worker/SharedWorker-type-mismatch.html b/Tests/LibWeb/Text/input/Worker/SharedWorker-type-mismatch.html new file mode 100644 index 0000000000..a17f528ecf --- /dev/null +++ b/Tests/LibWeb/Text/input/Worker/SharedWorker-type-mismatch.html @@ -0,0 +1,51 @@ + + +