Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for messaging between different browsing contexts #29803

Merged
merged 6 commits into from
Dec 9, 2021

Conversation

jgraham
Copy link
Contributor

@jgraham jgraham commented Jul 27, 2021

This will be split up to land, and will have one or more associated RFCs.

The features added are:

  • Support for giving contexts known ids through a uuid parameter in the URL.
  • Support for executing script in remote contexts using testdriver, and an implementation for wptrunner based on WebDriver.
  • Support for cross-context messaging based on send/poll/recv methods in testdriver, using the server stash as a backend.

// This ensures that no subsequent commands will be buffered in the underlying
// websocket before the navigation starts. This has to be done in the caller
// to avoid a race condition between the caller sending a new command and the
// socket being closed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In #28950,

  • I have something similar to closeAllChannelSockets() that suspends polling network requests on the remote context side,
  • but no pause()-ish things, relying on that the caller doesn't send any new commands until the page becomes inactive

so I'm curious how pause() works.
Particularly, are both of pause() and closeAllChannelSockets() needed?
Is there a race condition between these two?

},
{once: true});

async function navigateTo(url) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there are ways to trigger navigation other than location.href (e.g. form submission), I'd keep prepateNavigation() and execute_script() for navigation purpose.

Copy link
Member

@foolip foolip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot! I've reviewed up to infrastructure/channels/serialize_child.html...

infrastructure/channels/serialize_child.html Outdated Show resolved Hide resolved
infrastructure/channels/serialize_child.html Outdated Show resolved Hide resolved
infrastructure/channels/serialize_child.html Show resolved Hide resolved
infrastructure/channels/serialize_child.html Outdated Show resolved Hide resolved
infrastructure/channels/serialize_child.html Outdated Show resolved Hide resolved
@jgraham jgraham force-pushed the testdriver_cross_group branch 3 times, most recently from 44fd479 to bfaf63c Compare November 25, 2021 16:18
@jgraham jgraham requested a review from foolip November 30, 2021 11:45
Copy link
Member

@foolip foolip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed infrastructure/ and resources/, only took me 2+ hours.

infrastructure/channels/child_script.html Outdated Show resolved Hide resolved

let lastData;

// Hack: these will be recreated as assert failures in the caller
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Can you elaborate a bit on this comment so that it's easier to understand why this has the effect it has, and what the vision for the future is?

infrastructure/channels/serialize_child.html Show resolved Hide resolved
infrastructure/channels/serialize_child.html Outdated Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
typeof self[item.value.type] === "function") {
result = new self[item.value.type](item.value.message);
} else {
result = new Error(item.value.message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If item.value.type is "Error", the above case should be taken. Can you add a comment that this isn't about deserializing Error objects, but non-Error values?

resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
Copy link
Member

@foolip foolip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I've reviewed the Python parts (tools/ and websockets/) too now.

tools/wptrunner/requirements.txt Outdated Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
Copy link
Member

@foolip foolip left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that is left is some minor things.

Thanks for getting through this, it must have taken much longer to write than to review :D

resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
resources/channel.sub.js Outdated Show resolved Hide resolved
resources/channel.sub.js Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
websockets/handlers/msg_channel_wsh.py Outdated Show resolved Hide resolved
This allows stash-owned queues that can then be accessed across
multiple servers etc. In particular it can be used as the backend for
a shared message queue in which multiple browsing contexts can send
messages using the stash.
This allows creating message channels that can cross multiple browsing
contexts or script execution environments.

A simple channel pair can be created using the channel function e.g.

let [recvChan, sendChan] = await channel();

This generates a channel pair such that messages sent on sendChan will
arrive on recvChan.

Channels are multiple producer, single consumer, so there can only be
a single ReadChannel for each network, but multiple SendChannels.

On top of this underlying abstraction there's a higher level API which
provides known message structures. There is currently a command
message type where each command has an id, a method, a set of params,
and a respChannel which is a channel to use to reply to the command.

Currently two commands are provided: call, which runs a function in a
remote context, and postMessage, which sends a message to a remote
context. Messages and script parameters are serialized using a
WebDriver BiDi like remote value serialization algorithm which allows
passing complex objects over the wire.

To work with multiple contexts, one takes the following actions:

* In the context that will recieve messages, use:

  let recvChannel = await start_global_channel();

  This starts listening for messages on a channel defined by the
  testdriver context id (typically in the URL; maybe this should just
  be always in the URL which could also remove the testdriver.js
  dependency).

  If only executeScript is being used nothing more needs to be
  done. For postMessage, one can register message handlers using
  `recvChannel.addMessageHandler("message", fn)`.

* In the context that will send messages, use the uuid of the remote
  context to create a RemoteGlobal object. This will allow sending
  messages to the remote.

  let remote = RemoteGlobal(uuid);
  let result = remote.call(selector =>
    document.querySelector(selector).textContent,
    "body")

  remote.postMessage("done");

Message channels are backed with websockets, which use the stash as
the data storage layer. For use cases where it's important to close
all websockets (e.g. testing bfcache), the closeAllChannelSockets
helper is provided.
These aren't tests, so it can be assumed we're using it carefully.
The current setup assumed that we had one Stash instance per process
and so if the lock property on the class was initialised then we must
also have initialised the manager. The way that websockets handlers
are loaded breaks this assumption, so we need to ensure that the
lifecycle of the lock and of the manager object are identical. Sharing
the manager state between instances makes sense since we're already
sharing the proxy we get from the stash.
@jgraham jgraham merged commit 896ec65 into master Dec 9, 2021
@jgraham jgraham deleted the testdriver_cross_group branch December 9, 2021 15:57
@jgraham
Copy link
Contributor Author

jgraham commented Dec 9, 2021

Thanks for all the reviews @foolip!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs html infra testharness.js websockets wg-s_htmlwg wptrunner The automated test runner, commonly called through ./wpt run wptserve
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants