mirror of
https://github.com/RGBCube/serenity
synced 2025-05-25 15:45:08 +00:00
LibWeb: Implement MessagePort.postMessage closer to the spec
Use a LocalSocket to represent the connection between two message ports. The concept of the port message queue is still missing, however. When that concept is implemented, the "steps" in step 7 of the message port transfer steps will need to send the serialized data over the connected socketpair and run in the event loop of the process that holds onto the other side of the message port. Doing this should allow centralizing the behavior of postMessage for Window, MessagePorts and Workers.
This commit is contained in:
parent
c7f7ee2c4b
commit
f68c67bf3f
7 changed files with 142 additions and 33 deletions
|
@ -22,7 +22,7 @@ public:
|
|||
virtual WebIDL::ExceptionOr<void> transfer_steps(HTML::TransferDataHolder&) = 0;
|
||||
|
||||
// NOTE: It is an error to call Base::transfer_receiving_steps in your impl
|
||||
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder const&) = 0;
|
||||
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder&) = 0;
|
||||
|
||||
virtual HTML::TransferType primary_interface() const = 0;
|
||||
|
||||
|
|
|
@ -4,16 +4,24 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibCore/Socket.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibIPC/File.h>
|
||||
#include <LibWeb/Bindings/ExceptionOrUtils.h>
|
||||
#include <LibWeb/Bindings/Intrinsics.h>
|
||||
#include <LibWeb/Bindings/MessagePortPrototype.h>
|
||||
#include <LibWeb/DOM/EventDispatcher.h>
|
||||
#include <LibWeb/HTML/EventHandler.h>
|
||||
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
||||
#include <LibWeb/HTML/EventNames.h>
|
||||
#include <LibWeb/HTML/MessageEvent.h>
|
||||
#include <LibWeb/HTML/MessagePort.h>
|
||||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
||||
constexpr u8 IPC_FILE_TAG = 0xA5;
|
||||
|
||||
JS_DEFINE_ALLOCATOR(MessagePort);
|
||||
|
||||
JS::NonnullGCPtr<MessagePort> MessagePort::create(JS::Realm& realm)
|
||||
|
@ -41,7 +49,7 @@ void MessagePort::visit_edges(Cell::Visitor& visitor)
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#message-ports:transfer-steps
|
||||
WebIDL::ExceptionOr<void> MessagePort::transfer_steps(HTML::TransferDataHolder&)
|
||||
WebIDL::ExceptionOr<void> MessagePort::transfer_steps(HTML::TransferDataHolder& data_holder)
|
||||
{
|
||||
// 1. Set value's has been shipped flag to true.
|
||||
m_has_been_shipped = true;
|
||||
|
@ -55,18 +63,21 @@ WebIDL::ExceptionOr<void> MessagePort::transfer_steps(HTML::TransferDataHolder&)
|
|||
m_remote_port->m_has_been_shipped = true;
|
||||
|
||||
// 2. Set dataHolder.[[RemotePort]] to remotePort.
|
||||
// FIXME: Append an IPC::File to the dataHolder
|
||||
auto fd = MUST(m_socket->release_fd());
|
||||
m_socket = nullptr;
|
||||
data_holder.fds.append(fd);
|
||||
data_holder.data.append(IPC_FILE_TAG);
|
||||
}
|
||||
|
||||
// 4. Otherwise, set dataHolder.[[RemotePort]] to null.
|
||||
else {
|
||||
// FIXME: Note in the dataHolder that there are no fds
|
||||
data_holder.data.append(0);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
WebIDL::ExceptionOr<void> MessagePort::transfer_receiving_steps(HTML::TransferDataHolder const&)
|
||||
WebIDL::ExceptionOr<void> MessagePort::transfer_receiving_steps(HTML::TransferDataHolder& data_holder)
|
||||
{
|
||||
// 1. Set value's has been shipped flag to true.
|
||||
m_has_been_shipped = true;
|
||||
|
@ -75,8 +86,16 @@ WebIDL::ExceptionOr<void> MessagePort::transfer_receiving_steps(HTML::TransferDa
|
|||
// if any, leaving value's port message queue in its initial disabled state, and, if value's relevant global object is a Window,
|
||||
// associating the moved tasks with value's relevant global object's associated Document.
|
||||
|
||||
// FIXME: 3. If dataHolder.[[RemotePort]] is not null, then entangle dataHolder.[[RemotePort]] and value.
|
||||
// 3. If dataHolder.[[RemotePort]] is not null, then entangle dataHolder.[[RemotePort]] and value.
|
||||
// (This will disentangle dataHolder.[[RemotePort]] from the original port that was transferred.)
|
||||
auto fd_tag = data_holder.data.take_first();
|
||||
if (fd_tag == IPC_FILE_TAG) {
|
||||
auto fd = data_holder.fds.take_first();
|
||||
m_socket = MUST(Core::LocalSocket::adopt_fd(fd.take_fd()));
|
||||
} else if (fd_tag != 0) {
|
||||
dbgln("Unexpected byte {:x} in MessagePort transfer data", fd_tag);
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
@ -85,6 +104,8 @@ void MessagePort::disentangle()
|
|||
{
|
||||
m_remote_port->m_remote_port = nullptr;
|
||||
m_remote_port = nullptr;
|
||||
|
||||
m_socket = nullptr;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#entangle
|
||||
|
@ -103,50 +124,129 @@ void MessagePort::entangle_with(MessagePort& remote_port)
|
|||
// (There is no MessageChannel object that represents this channel.)
|
||||
remote_port.m_remote_port = this;
|
||||
m_remote_port = &remote_port;
|
||||
|
||||
int fds[2] = {};
|
||||
MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, fds));
|
||||
auto socket0 = MUST(Core::LocalSocket::adopt_fd(fds[0]));
|
||||
MUST(socket0->set_blocking(false));
|
||||
MUST(socket0->set_close_on_exec(true));
|
||||
auto socket1 = MUST(Core::LocalSocket::adopt_fd(fds[1]));
|
||||
MUST(socket1->set_blocking(false));
|
||||
MUST(socket1->set_close_on_exec(true));
|
||||
|
||||
m_socket = move(socket0);
|
||||
m_remote_port->m_socket = move(socket1);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage-options
|
||||
WebIDL::ExceptionOr<void> MessagePort::post_message(JS::Value message, Vector<JS::Handle<JS::Object>> const& transfer)
|
||||
{
|
||||
// 1. Let targetPort be the port with which this MessagePort is entangled, if any; otherwise let it be null.
|
||||
JS::GCPtr<MessagePort> target_port = m_remote_port;
|
||||
|
||||
// 2. Let options be «[ "transfer" → transfer ]».
|
||||
auto options = StructuredSerializeOptions { transfer };
|
||||
|
||||
// 3. Run the message port post message steps providing this, targetPort, message and options.
|
||||
return message_port_post_message_steps(target_port, message, options);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage
|
||||
void MessagePort::post_message(JS::Value message)
|
||||
WebIDL::ExceptionOr<void> MessagePort::post_message(JS::Value message, StructuredSerializeOptions const& options)
|
||||
{
|
||||
// 1. Let targetPort be the port with which this MessagePort is entangled, if any; otherwise let it be null.
|
||||
auto* target_port = m_remote_port.ptr();
|
||||
JS::GCPtr<MessagePort> target_port = m_remote_port;
|
||||
|
||||
// FIXME: 2. Let options be «[ "transfer" → transfer ]».
|
||||
|
||||
// 3. Run the message port post message steps providing targetPort, message and options.
|
||||
// 2. Run the message port post message steps providing targetPort, message and options.
|
||||
return message_port_post_message_steps(target_port, message, options);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#message-port-post-message-steps
|
||||
WebIDL::ExceptionOr<void> MessagePort::message_port_post_message_steps(JS::GCPtr<MessagePort> target_port, JS::Value message, StructuredSerializeOptions const& options)
|
||||
{
|
||||
auto& realm = this->realm();
|
||||
auto& vm = this->vm();
|
||||
|
||||
// FIXME: 1. Let transfer be options["transfer"].
|
||||
// 1. Let transfer be options["transfer"].
|
||||
auto const& transfer = options.transfer;
|
||||
|
||||
// FIXME: 2. If transfer contains this MessagePort, then throw a "DataCloneError" DOMException.
|
||||
// 2. If transfer contains this MessagePort, then throw a "DataCloneError" DOMException.
|
||||
for (auto const& handle : transfer) {
|
||||
if (handle == this)
|
||||
return WebIDL::DataCloneError::create(realm, "Cannot transfer a MessagePort to itself"_fly_string);
|
||||
}
|
||||
|
||||
// 3. Let doomed be false.
|
||||
bool doomed = false;
|
||||
|
||||
// FIXME: 4. If targetPort is not null and transfer contains targetPort, then set doomed to true and optionally report to a developer console that the target port was posted to itself, causing the communication channel to be lost.
|
||||
// 4. If targetPort is not null and transfer contains targetPort, then set doomed to true and optionally report to a developer console that the target port was posted to itself, causing the communication channel to be lost.
|
||||
if (target_port) {
|
||||
for (auto const& handle : transfer) {
|
||||
if (handle == target_port.ptr()) {
|
||||
doomed = true;
|
||||
dbgln("FIXME: Report to a developer console that the target port was posted to itself, causing the communication channel to be lost");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: 5. Let serializeWithTransferResult be StructuredSerializeWithTransfer(message, transfer). Rethrow any exceptions.
|
||||
// 5. Let serializeWithTransferResult be StructuredSerializeWithTransfer(message, transfer). Rethrow any exceptions.
|
||||
auto serialize_with_transfer_result = TRY(structured_serialize_with_transfer(vm, message, transfer));
|
||||
|
||||
// 6. If targetPort is null, or if doomed is true, then return.
|
||||
if (!target_port || doomed)
|
||||
return;
|
||||
return {};
|
||||
|
||||
// FIXME: 7. Add a task that runs the following steps to the port message queue of targetPort:
|
||||
// FIXME: Implement this using the port message queue/unshipped port message queue concept
|
||||
main_thread_event_loop().task_queue().add(HTML::Task::create(HTML::Task::Source::PostedMessage, nullptr, [target_port, serialize_with_transfer_result = move(serialize_with_transfer_result)]() mutable {
|
||||
// FIXME: 1. Let finalTargetPort be the MessagePort in whose port message queue the task now finds itself.
|
||||
// FIXME: NOTE: This can be different from targetPort, if targetPort itself was transferred and thus all its tasks moved along with it.
|
||||
auto final_target_port = target_port;
|
||||
|
||||
// FIXME: This is an ad-hoc hack implementation instead, since we don't currently
|
||||
// have serialization and deserialization of messages.
|
||||
main_thread_event_loop().task_queue().add(HTML::Task::create(HTML::Task::Source::PostedMessage, nullptr, [target_port, message] {
|
||||
// 2. Let targetRealm be finalTargetPort's relevant realm.
|
||||
auto& target_realm = relevant_realm(*final_target_port);
|
||||
auto& target_vm = target_realm.vm();
|
||||
|
||||
// 3. Let deserializeRecord be StructuredDeserializeWithTransfer(serializeWithTransferResult, targetRealm).
|
||||
TemporaryExecutionContext context { relevant_settings_object(*final_target_port) };
|
||||
auto deserialize_record_or_error = structured_deserialize_with_transfer(target_vm, serialize_with_transfer_result);
|
||||
if (deserialize_record_or_error.is_error()) {
|
||||
// If this throws an exception, catch it, fire an event named messageerror at finalTargetPort, using MessageEvent, and then return.
|
||||
auto exception = deserialize_record_or_error.release_error();
|
||||
MessageEventInit event_init {};
|
||||
event_init.data = message;
|
||||
event_init.origin = "<origin>"_string;
|
||||
target_port->dispatch_event(MessageEvent::create(target_port->realm(), HTML::EventNames::message, event_init));
|
||||
}));
|
||||
final_target_port->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::messageerror, event_init));
|
||||
return;
|
||||
}
|
||||
auto deserialize_record = deserialize_record_or_error.release_value();
|
||||
|
||||
// 4. Let messageClone be deserializeRecord.[[Deserialized]].
|
||||
auto message_clone = deserialize_record.deserialized;
|
||||
|
||||
// 5. Let newPorts be a new frozen array consisting of all MessagePort objects in deserializeRecord.[[TransferredValues]], if any, maintaining their relative order.
|
||||
// FIXME: Use a FrozenArray
|
||||
Vector<JS::Handle<JS::Object>> new_ports;
|
||||
for (auto const& object : deserialize_record.transferred_values) {
|
||||
if (is<HTML::MessagePort>(*object)) {
|
||||
new_ports.append(object);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Fire an event named message at finalTargetPort, using MessageEvent, with the data attribute initialized to messageClone and the ports attribute initialized to newPorts.
|
||||
MessageEventInit event_init {};
|
||||
event_init.data = message_clone;
|
||||
event_init.ports = move(new_ports);
|
||||
final_target_port->dispatch_event(MessageEvent::create(target_realm, HTML::EventNames::message, event_init));
|
||||
}));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-start
|
||||
void MessagePort::start()
|
||||
{
|
||||
// FIXME: Message ports are supposed to be disabled by default.
|
||||
VERIFY(m_socket);
|
||||
|
||||
// TODO: The start() method steps are to enable this's port message queue, if it is not already enabled.
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-close
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/Weakable.h>
|
||||
#include <LibCore/Socket.h>
|
||||
#include <LibWeb/Bindings/Transferable.h>
|
||||
#include <LibWeb/DOM/EventTarget.h>
|
||||
#include <LibWeb/Forward.h>
|
||||
|
@ -38,7 +39,10 @@ public:
|
|||
void entangle_with(MessagePort&);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage
|
||||
void post_message(JS::Value);
|
||||
WebIDL::ExceptionOr<void> post_message(JS::Value message, Vector<JS::Handle<JS::Object>> const& transfer);
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#dom-messageport-postmessage-options
|
||||
WebIDL::ExceptionOr<void> post_message(JS::Value message, StructuredSerializeOptions const& options);
|
||||
|
||||
void start();
|
||||
|
||||
|
@ -53,7 +57,7 @@ public:
|
|||
|
||||
// ^Transferable
|
||||
virtual WebIDL::ExceptionOr<void> transfer_steps(HTML::TransferDataHolder&) override;
|
||||
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder const&) override;
|
||||
virtual WebIDL::ExceptionOr<void> transfer_receiving_steps(HTML::TransferDataHolder&) override;
|
||||
virtual HTML::TransferType primary_interface() const override { return HTML::TransferType::MessagePort; }
|
||||
|
||||
private:
|
||||
|
@ -62,14 +66,18 @@ private:
|
|||
virtual void initialize(JS::Realm&) override;
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
bool is_entangled() const { return m_remote_port; }
|
||||
bool is_entangled() const { return static_cast<bool>(m_socket); }
|
||||
void disentangle();
|
||||
|
||||
WebIDL::ExceptionOr<void> message_port_post_message_steps(JS::GCPtr<MessagePort> target_port, JS::Value message, StructuredSerializeOptions const& options);
|
||||
|
||||
// The HTML spec implies(!) that this is MessagePort.[[RemotePort]]
|
||||
JS::GCPtr<MessagePort> m_remote_port;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/web-messaging.html#has-been-shipped
|
||||
bool m_has_been_shipped { false };
|
||||
|
||||
OwnPtr<Core::LocalSocket> m_socket;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
// https://html.spec.whatwg.org/multipage/web-messaging.html#messageport
|
||||
[Exposed=(Window,Worker,AudioWorklet), Transferable]
|
||||
interface MessagePort : EventTarget {
|
||||
undefined postMessage(any message);
|
||||
undefined postMessage(any message, sequence<object> transfer);
|
||||
undefined postMessage(any message, optional StructuredSerializeOptions options = {});
|
||||
undefined start();
|
||||
undefined close();
|
||||
|
||||
|
|
|
@ -1019,7 +1019,7 @@ static bool is_interface_exposed_on_target_realm(u8 name, JS::Realm& realm)
|
|||
return false;
|
||||
}
|
||||
|
||||
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Bindings::PlatformObject>> create_transferred_value(TransferType name, JS::Realm& target_realm, TransferDataHolder const& transfer_data_holder)
|
||||
static WebIDL::ExceptionOr<JS::NonnullGCPtr<Bindings::PlatformObject>> create_transferred_value(TransferType name, JS::Realm& target_realm, TransferDataHolder& transfer_data_holder)
|
||||
{
|
||||
switch (name) {
|
||||
case TransferType::MessagePort: {
|
||||
|
@ -1032,7 +1032,7 @@ static WebIDL::ExceptionOr<JS::NonnullGCPtr<Bindings::PlatformObject>> create_tr
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/structured-data.html#structureddeserializewithtransfer
|
||||
WebIDL::ExceptionOr<DeserializedTransferRecord> structured_deserialize_with_transfer(JS::VM& vm, SerializedTransferRecord const& serialize_with_transfer_result)
|
||||
WebIDL::ExceptionOr<DeserializedTransferRecord> structured_deserialize_with_transfer(JS::VM& vm, SerializedTransferRecord& serialize_with_transfer_result)
|
||||
{
|
||||
auto& target_realm = *vm.current_realm();
|
||||
|
||||
|
@ -1068,7 +1068,7 @@ WebIDL::ExceptionOr<DeserializedTransferRecord> structured_deserialize_with_tran
|
|||
// 4. Otherwise:
|
||||
else {
|
||||
// 1. Let interfaceName be transferDataHolder.[[Type]].
|
||||
u8 const interface_name = transfer_data_holder.data[0];
|
||||
u8 const interface_name = transfer_data_holder.data.take_first();
|
||||
|
||||
// 2. If the interface identified by interfaceName is not exposed in targetRealm, then throw a "DataCloneError" DOMException.
|
||||
if (!is_interface_exposed_on_target_realm(interface_name, target_realm))
|
||||
|
|
|
@ -51,6 +51,6 @@ WebIDL::ExceptionOr<SerializationRecord> structured_serialize_internal(JS::VM& v
|
|||
WebIDL::ExceptionOr<JS::Value> structured_deserialize(JS::VM& vm, SerializationRecord const& serialized, JS::Realm& target_realm, Optional<DeserializationMemory>);
|
||||
|
||||
WebIDL::ExceptionOr<SerializedTransferRecord> structured_serialize_with_transfer(JS::VM& vm, JS::Value value, Vector<JS::Handle<JS::Object>> const& transfer_list);
|
||||
WebIDL::ExceptionOr<DeserializedTransferRecord> structured_deserialize_with_transfer(JS::VM& vm, SerializedTransferRecord const&);
|
||||
WebIDL::ExceptionOr<DeserializedTransferRecord> structured_deserialize_with_transfer(JS::VM& vm, SerializedTransferRecord&);
|
||||
|
||||
}
|
||||
|
|
|
@ -1053,7 +1053,7 @@ WebIDL::ExceptionOr<void> Window::window_post_message_steps(JS::Value message, W
|
|||
auto serialize_with_transfer_result = TRY(structured_serialize_with_transfer(target_realm.vm(), message, transfer));
|
||||
|
||||
// 8. Queue a global task on the posted message task source given targetWindow to run the following steps:
|
||||
queue_global_task(Task::Source::PostedMessage, *this, [this, serialize_with_transfer_result = move(serialize_with_transfer_result), target_origin = move(target_origin), &incumbent_settings, &target_realm]() {
|
||||
queue_global_task(Task::Source::PostedMessage, *this, [this, serialize_with_transfer_result = move(serialize_with_transfer_result), target_origin = move(target_origin), &incumbent_settings, &target_realm]() mutable {
|
||||
// 1. If the targetOrigin argument is not a single literal U+002A ASTERISK character (*) and targetWindow's
|
||||
// associated Document's origin is not same origin with targetOrigin, then return.
|
||||
// NOTE: Due to step 4 and 5 above, the only time it's not '*' is if target_origin contains an Origin.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue