Commit abacd4c1 authored by Andrey Kosyakov's avatar Andrey Kosyakov Committed by Commit Bot

DevTools: add support for injecting bindings by context name

This adds support for injecting binding into contexts other than
main based on the context name (AKA isolated world name in Blink
terms). This would simplify a common use case for addBinding in
Puppeteer and other automation tools that use addBinding to expose
a back-channel for extension code running in an isolated world by
making bindings available to such code at an early stage and in a
race-free manner (currently, we can only inject a binding into
specific context after the creation of the context has been reported
to the client, which typically introduces a race with other evals
the client may be running in the context).

Change-Id: I66454954491a47a0c9aa4864f0aace4da2e67d3a
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2440984Reviewed-by: 's avatarSimon Zünd <szuend@chromium.org>
Reviewed-by: 's avatarPavel Feldman <pfeldman@chromium.org>
Commit-Queue: Andrey Kosyakov <caseq@chromium.org>
Cr-Commit-Position: refs/heads/master@{#70266}
parent 179f7f43
...@@ -1542,15 +1542,23 @@ domain Runtime ...@@ -1542,15 +1542,23 @@ domain Runtime
# If executionContextId is empty, adds binding with the given name on the # If executionContextId is empty, adds binding with the given name on the
# global objects of all inspected contexts, including those created later, # global objects of all inspected contexts, including those created later,
# bindings survive reloads. # bindings survive reloads.
# If executionContextId is specified, adds binding only on global object of
# given execution context.
# Binding function takes exactly one argument, this argument should be string, # Binding function takes exactly one argument, this argument should be string,
# in case of any other input, function throws an exception. # in case of any other input, function throws an exception.
# Each binding function call produces Runtime.bindingCalled notification. # Each binding function call produces Runtime.bindingCalled notification.
experimental command addBinding experimental command addBinding
parameters parameters
string name string name
# If specified, the binding would only be exposed to the specified
# execution context. If omitted and `executionContextName` is not set,
# the binding is exposed to all execution contexts of the target.
# This parameter is mutually exclusive with `executionContextName`.
optional ExecutionContextId executionContextId optional ExecutionContextId executionContextId
# If specified, the binding is exposed to the executionContext with
# matching name, even for contexts created after the binding is added.
# See also `ExecutionContext.name` and `worldName` parameter to
# `Page.addScriptToEvaluateOnNewDocument`.
# This parameter is mutually exclusive with `executionContextId`.
experimental optional string executionContextName
# This method does not remove binding function from global object but # This method does not remove binding function from global object but
# unsubscribes current runtime agent from Runtime.bindingCalled notifications. # unsubscribes current runtime agent from Runtime.bindingCalled notifications.
......
...@@ -56,6 +56,7 @@ static const char customObjectFormatterEnabled[] = ...@@ -56,6 +56,7 @@ static const char customObjectFormatterEnabled[] =
"customObjectFormatterEnabled"; "customObjectFormatterEnabled";
static const char runtimeEnabled[] = "runtimeEnabled"; static const char runtimeEnabled[] = "runtimeEnabled";
static const char bindings[] = "bindings"; static const char bindings[] = "bindings";
static const char globalBindingsKey[] = "";
} // namespace V8RuntimeAgentImplState } // namespace V8RuntimeAgentImplState
using protocol::Runtime::RemoteObject; using protocol::Runtime::RemoteObject;
...@@ -663,32 +664,61 @@ void V8RuntimeAgentImpl::terminateExecution( ...@@ -663,32 +664,61 @@ void V8RuntimeAgentImpl::terminateExecution(
m_inspector->debugger()->terminateExecution(std::move(callback)); m_inspector->debugger()->terminateExecution(std::move(callback));
} }
namespace {
protocol::DictionaryValue* getOrCreateDictionary(
protocol::DictionaryValue* dict, const String16& key) {
if (protocol::DictionaryValue* bindings = dict->getObject(key))
return bindings;
dict->setObject(key, protocol::DictionaryValue::create());
return dict->getObject(key);
}
} // namespace
Response V8RuntimeAgentImpl::addBinding(const String16& name, Response V8RuntimeAgentImpl::addBinding(const String16& name,
Maybe<int> executionContextId) { Maybe<int> executionContextId,
Maybe<String16> executionContextName) {
if (m_activeBindings.count(name)) return Response::Success(); if (m_activeBindings.count(name)) return Response::Success();
if (executionContextId.isJust()) { if (executionContextId.isJust()) {
if (executionContextName.isJust()) {
return Response::InvalidParams(
"executionContextName is mutually exclusive with executionContextId");
}
int contextId = executionContextId.fromJust(); int contextId = executionContextId.fromJust();
InspectedContext* context = InspectedContext* context =
m_inspector->getContext(m_session->contextGroupId(), contextId); m_inspector->getContext(m_session->contextGroupId(), contextId);
if (!context) { if (!context) {
return Response::ServerError( return Response::InvalidParams(
"Cannot find execution context with given executionContextId"); "Cannot find execution context with given executionContextId");
} }
addBinding(context, name); addBinding(context, name);
return Response::Success(); return Response::Success();
} }
// If it's a globally exposed binding, i.e. no context name specified, use
// a special value for the context name.
String16 contextKey = V8RuntimeAgentImplState::globalBindingsKey;
if (executionContextName.isJust()) {
contextKey = executionContextName.fromJust();
if (contextKey == V8RuntimeAgentImplState::globalBindingsKey) {
return Response::InvalidParams("Invalid executionContextName");
}
}
// Only persist non context-specific bindings, as contextIds don't make // Only persist non context-specific bindings, as contextIds don't make
// any sense when state is restored in a different process. // any sense when state is restored in a different process.
if (!m_state->getObject(V8RuntimeAgentImplState::bindings)) {
m_state->setObject(V8RuntimeAgentImplState::bindings,
protocol::DictionaryValue::create());
}
protocol::DictionaryValue* bindings = protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings); getOrCreateDictionary(m_state, V8RuntimeAgentImplState::bindings);
bindings->setBoolean(name, true); protocol::DictionaryValue* contextBindings =
getOrCreateDictionary(bindings, contextKey);
contextBindings->setBoolean(name, true);
m_inspector->forEachContext( m_inspector->forEachContext(
m_session->contextGroupId(), m_session->contextGroupId(),
[&name, this](InspectedContext* context) { addBinding(context, name); }); [&name, &executionContextName, this](InspectedContext* context) {
if (executionContextName.isJust() &&
executionContextName.fromJust() != context->humanReadableName())
return;
addBinding(context, name);
});
return Response::Success(); return Response::Success();
} }
...@@ -750,12 +780,23 @@ void V8RuntimeAgentImpl::bindingCalled(const String16& name, ...@@ -750,12 +780,23 @@ void V8RuntimeAgentImpl::bindingCalled(const String16& name,
} }
void V8RuntimeAgentImpl::addBindings(InspectedContext* context) { void V8RuntimeAgentImpl::addBindings(InspectedContext* context) {
const String16 contextName = context->humanReadableName();
if (!m_enabled) return; if (!m_enabled) return;
protocol::DictionaryValue* bindings = protocol::DictionaryValue* bindings =
m_state->getObject(V8RuntimeAgentImplState::bindings); m_state->getObject(V8RuntimeAgentImplState::bindings);
if (!bindings) return; if (!bindings) return;
for (size_t i = 0; i < bindings->size(); ++i) protocol::DictionaryValue* globalBindings =
addBinding(context, bindings->at(i).first); bindings->getObject(V8RuntimeAgentImplState::globalBindingsKey);
if (globalBindings) {
for (size_t i = 0; i < globalBindings->size(); ++i)
addBinding(context, globalBindings->at(i).first);
}
protocol::DictionaryValue* contextBindings =
contextName.isEmpty() ? nullptr : bindings->getObject(contextName);
if (contextBindings) {
for (size_t i = 0; i < contextBindings->size(); ++i)
addBinding(context, contextBindings->at(i).first);
}
} }
void V8RuntimeAgentImpl::restore() { void V8RuntimeAgentImpl::restore() {
......
...@@ -117,8 +117,8 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend { ...@@ -117,8 +117,8 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend {
void terminateExecution( void terminateExecution(
std::unique_ptr<TerminateExecutionCallback> callback) override; std::unique_ptr<TerminateExecutionCallback> callback) override;
Response addBinding(const String16& name, Response addBinding(const String16& name, Maybe<int> executionContextId,
Maybe<int> executionContextId) override; Maybe<String16> executionContextName) override;
Response removeBinding(const String16& name) override; Response removeBinding(const String16& name) override;
void addBindings(InspectedContext* context); void addBindings(InspectedContext* context);
......
...@@ -156,3 +156,51 @@ binding called in session1 ...@@ -156,3 +156,51 @@ binding called in session1
} }
} }
Call binding in newly created context (binding should NOT be exposed) Call binding in newly created context (binding should NOT be exposed)
Running test: testAddBindingToContextByName
Call binding in default context (binding should NOT be exposed)
Call binding in Foo (binding should be exposed)
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : frobnicate
payload : message
}
}
Call binding in Bar (binding should NOT be exposed)
Call binding in newly-created Foo (binding should be exposed)
binding called in session1
{
method : Runtime.bindingCalled
params : {
executionContextId : <executionContextId>
name : frobnicate
payload : message
}
}
Call binding in newly-created Bazz (binding should NOT be exposed)
Running test: testErrors
{
error : {
code : -32602
message : Invalid executionContextName
}
id : <messageId>
}
{
error : {
code : -32602
message : executionContextName is mutually exclusive with executionContextId
}
id : <messageId>
}
{
error : {
code : -32602
message : Cannot find execution context with given executionContextId
}
id : <messageId>
}
...@@ -85,7 +85,53 @@ InspectorTest.runAsyncTestSuite([ ...@@ -85,7 +85,53 @@ InspectorTest.runAsyncTestSuite([
contextGroup.createContext(); contextGroup.createContext();
const contextId3 = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id; const contextId3 = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
await session.Protocol.Runtime.evaluate({expression, contextId: contextId3}); await session.Protocol.Runtime.evaluate({expression, contextId: contextId3});
},
async function testAddBindingToContextByName() {
const {contextGroup, sessions: [session]} = setupSessions(1);
const defaultContext = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
contextGroup.createContext("foo");
const contextFoo = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
contextGroup.createContext("bar");
const contextBar = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
await session.Protocol.Runtime.addBinding({name: 'frobnicate', executionContextName: 'foo'});
const expression = `frobnicate('message')`;
InspectorTest.log('Call binding in default context (binding should NOT be exposed)');
await session.Protocol.Runtime.evaluate({expression});
InspectorTest.log('Call binding in Foo (binding should be exposed)');
await session.Protocol.Runtime.evaluate({expression, contextId: contextFoo});
InspectorTest.log('Call binding in Bar (binding should NOT be exposed)');
await session.Protocol.Runtime.evaluate({expression, contextId: contextBar});
contextGroup.createContext("foo");
const contextFoo2 = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
InspectorTest.log('Call binding in newly-created Foo (binding should be exposed)');
await session.Protocol.Runtime.evaluate({expression, contextId: contextFoo2});
contextGroup.createContext("bazz");
const contextBazz = (await session.Protocol.Runtime.onceExecutionContextCreated()).params.context.id;
InspectorTest.log('Call binding in newly-created Bazz (binding should NOT be exposed)');
await session.Protocol.Runtime.evaluate({expression, contextId: contextBazz});
},
async function testErrors() {
const {contextGroup, sessions: [session]} = setupSessions(1);
let err = await session.Protocol.Runtime.addBinding({name: 'frobnicate', executionContextName: ''});
InspectorTest.logMessage(err);
err = await session.Protocol.Runtime.addBinding({name: 'frobnicate', executionContextName: 'foo', executionContextId: 1});
InspectorTest.logMessage(err);
err = await session.Protocol.Runtime.addBinding({name: 'frobnicate', executionContextId: 2128506});
InspectorTest.logMessage(err);
} }
]); ]);
function setupSessions(num) { function setupSessions(num) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment