Skip to content

Commit d89f9f8

Browse files
Jan Kremsaddaleax
Jan Krems
authored andcommitted
inspector: allow require in Runtime.evaluate
Some parts were written by Timothy Gu <[email protected]>. PR-URL: #8837 Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Aleksei Koziatinskii <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Timothy Gu <[email protected]>
1 parent 96147c9 commit d89f9f8

File tree

4 files changed

+197
-0
lines changed

4 files changed

+197
-0
lines changed

lib/internal/bootstrap_node.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
return console;
283283
}
284284
});
285+
setupInspectorCommandLineAPI();
285286
}
286287

287288
function installInspectorConsole(globalConsole) {
@@ -310,6 +311,22 @@
310311
return wrappedConsole;
311312
}
312313

314+
function setupInspectorCommandLineAPI() {
315+
const { addCommandLineAPI } = process.binding('inspector');
316+
if (!addCommandLineAPI) return;
317+
318+
const Module = NativeModule.require('module');
319+
const { makeRequireFunction } = NativeModule.require('internal/module');
320+
const path = NativeModule.require('path');
321+
const cwd = tryGetCwd(path);
322+
323+
const consoleAPIModule = new Module('<inspector console>');
324+
consoleAPIModule.paths =
325+
Module._nodeModulePaths(cwd).concat(Module.globalPaths);
326+
327+
addCommandLineAPI('require', makeRequireFunction(consoleAPIModule));
328+
}
329+
313330
function setupProcessFatal() {
314331
const async_wrap = process.binding('async_wrap');
315332
// Arrays containing hook flags and ids for async_hook calls.

src/env.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ namespace node {
276276
V(context, v8::Context) \
277277
V(domain_array, v8::Array) \
278278
V(domains_stack_array, v8::Array) \
279+
V(inspector_console_api_object, v8::Object) \
279280
V(jsstream_constructor_template, v8::FunctionTemplate) \
280281
V(module_load_list_array, v8::Array) \
281282
V(pbkdf2_constructor_template, v8::ObjectTemplate) \

src/inspector_agent.cc

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
namespace node {
2323
namespace inspector {
2424
namespace {
25+
using v8::Array;
2526
using v8::Context;
2627
using v8::External;
2728
using v8::Function;
@@ -554,6 +555,20 @@ class NodeInspectorClient : public V8InspectorClient {
554555
return env_->context();
555556
}
556557

558+
void installAdditionalCommandLineAPI(Local<Context> context,
559+
Local<Object> target) override {
560+
Local<Object> console_api = env_->inspector_console_api_object();
561+
562+
Local<Array> properties =
563+
console_api->GetOwnPropertyNames(context).ToLocalChecked();
564+
for (uint32_t i = 0; i < properties->Length(); ++i) {
565+
Local<Value> key = properties->Get(context, i).ToLocalChecked();
566+
target->Set(context,
567+
key,
568+
console_api->Get(context, key).ToLocalChecked()).FromJust();
569+
}
570+
}
571+
557572
void FatalException(Local<Value> error, Local<v8::Message> message) {
558573
Local<Context> context = env_->context();
559574

@@ -682,6 +697,20 @@ bool Agent::StartIoThread(bool wait_for_connect) {
682697
return true;
683698
}
684699

700+
static void AddCommandLineAPI(
701+
const FunctionCallbackInfo<Value>& info) {
702+
auto env = Environment::GetCurrent(info);
703+
Local<Context> context = env->context();
704+
705+
if (info.Length() != 2 || !info[0]->IsString()) {
706+
return env->ThrowTypeError("inspector.addCommandLineAPI takes "
707+
"exactly 2 arguments: a string and a value.");
708+
}
709+
710+
Local<Object> console_api = env->inspector_console_api_object();
711+
console_api->Set(context, info[0], info[1]).FromJust();
712+
}
713+
685714
void Agent::Stop() {
686715
if (io_ != nullptr) {
687716
io_->Stop();
@@ -784,8 +813,16 @@ void Url(const FunctionCallbackInfo<Value>& args) {
784813
void Agent::InitInspector(Local<Object> target, Local<Value> unused,
785814
Local<Context> context, void* priv) {
786815
Environment* env = Environment::GetCurrent(context);
816+
{
817+
auto obj = Object::New(env->isolate());
818+
auto null = Null(env->isolate());
819+
CHECK(obj->SetPrototype(context, null).FromJust());
820+
env->set_inspector_console_api_object(obj);
821+
}
822+
787823
Agent* agent = env->inspector_agent();
788824
env->SetMethod(target, "consoleCall", InspectorConsoleCall);
825+
env->SetMethod(target, "addCommandLineAPI", AddCommandLineAPI);
789826
if (agent->debug_options_.wait_for_connect())
790827
env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart);
791828
env->SetMethod(target, "connect", ConnectJSBindingsSession);

test/inspector/test-inspector.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ function checkBadPath(err, response) {
3434
assert(/WebSockets request was expected/.test(err.response));
3535
}
3636

37+
function checkException(message) {
38+
assert.strictEqual(message['exceptionDetails'], undefined,
39+
'An exception occurred during execution');
40+
}
41+
3742
function expectMainScriptSource(result) {
3843
const expected = helper.mainScriptSource();
3944
const source = result['scriptSource'];
@@ -209,6 +214,142 @@ function testI18NCharacters(session) {
209214
]);
210215
}
211216

217+
function testCommandLineAPI(session) {
218+
const testModulePath = require.resolve('../fixtures/empty.js');
219+
const testModuleStr = JSON.stringify(testModulePath);
220+
const printAModulePath = require.resolve('../fixtures/printA.js');
221+
const printAModuleStr = JSON.stringify(printAModulePath);
222+
const printBModulePath = require.resolve('../fixtures/printB.js');
223+
const printBModuleStr = JSON.stringify(printBModulePath);
224+
session.sendInspectorCommands([
225+
[ // we can use `require` outside of a callframe with require in scope
226+
{
227+
'method': 'Runtime.evaluate', 'params': {
228+
'expression': 'typeof require("fs").readFile === "function"',
229+
'includeCommandLineAPI': true
230+
}
231+
}, (message) => {
232+
checkException(message);
233+
assert.strictEqual(message['result']['value'], true);
234+
}
235+
],
236+
[ // the global require has the same properties as a normal `require`
237+
{
238+
'method': 'Runtime.evaluate', 'params': {
239+
'expression': [
240+
'typeof require.resolve === "function"',
241+
'typeof require.extensions === "object"',
242+
'typeof require.cache === "object"'
243+
].join(' && '),
244+
'includeCommandLineAPI': true
245+
}
246+
}, (message) => {
247+
checkException(message);
248+
assert.strictEqual(message['result']['value'], true);
249+
}
250+
],
251+
[ // `require` twice returns the same value
252+
{
253+
'method': 'Runtime.evaluate', 'params': {
254+
// 1. We require the same module twice
255+
// 2. We mutate the exports so we can compare it later on
256+
'expression': `
257+
Object.assign(
258+
require(${testModuleStr}),
259+
{ old: 'yes' }
260+
) === require(${testModuleStr})`,
261+
'includeCommandLineAPI': true
262+
}
263+
}, (message) => {
264+
checkException(message);
265+
assert.strictEqual(message['result']['value'], true);
266+
}
267+
],
268+
[ // after require the module appears in require.cache
269+
{
270+
'method': 'Runtime.evaluate', 'params': {
271+
'expression': `JSON.stringify(
272+
require.cache[${testModuleStr}].exports
273+
)`,
274+
'includeCommandLineAPI': true
275+
}
276+
}, (message) => {
277+
checkException(message);
278+
assert.deepStrictEqual(JSON.parse(message['result']['value']),
279+
{ old: 'yes' });
280+
}
281+
],
282+
[ // remove module from require.cache
283+
{
284+
'method': 'Runtime.evaluate', 'params': {
285+
'expression': `delete require.cache[${testModuleStr}]`,
286+
'includeCommandLineAPI': true
287+
}
288+
}, (message) => {
289+
checkException(message);
290+
assert.strictEqual(message['result']['value'], true);
291+
}
292+
],
293+
[ // require again, should get fresh (empty) exports
294+
{
295+
'method': 'Runtime.evaluate', 'params': {
296+
'expression': `JSON.stringify(require(${testModuleStr}))`,
297+
'includeCommandLineAPI': true
298+
}
299+
}, (message) => {
300+
checkException(message);
301+
assert.deepStrictEqual(JSON.parse(message['result']['value']), {});
302+
}
303+
],
304+
[ // require 2nd module, exports an empty object
305+
{
306+
'method': 'Runtime.evaluate', 'params': {
307+
'expression': `JSON.stringify(require(${printAModuleStr}))`,
308+
'includeCommandLineAPI': true
309+
}
310+
}, (message) => {
311+
checkException(message);
312+
assert.deepStrictEqual(JSON.parse(message['result']['value']), {});
313+
}
314+
],
315+
[ // both modules end up with the same module.parent
316+
{
317+
'method': 'Runtime.evaluate', 'params': {
318+
'expression': `JSON.stringify({
319+
parentsEqual:
320+
require.cache[${testModuleStr}].parent ===
321+
require.cache[${printAModuleStr}].parent,
322+
parentId: require.cache[${testModuleStr}].parent.id,
323+
})`,
324+
'includeCommandLineAPI': true
325+
}
326+
}, (message) => {
327+
checkException(message);
328+
assert.deepStrictEqual(JSON.parse(message['result']['value']), {
329+
parentsEqual: true,
330+
parentId: '<inspector console>'
331+
});
332+
}
333+
],
334+
[ // the `require` in the module shadows the command line API's `require`
335+
{
336+
'method': 'Debugger.evaluateOnCallFrame', 'params': {
337+
'callFrameId': '{"ordinal":0,"injectedScriptId":1}',
338+
'expression': `(
339+
require(${printBModuleStr}),
340+
require.cache[${printBModuleStr}].parent.id
341+
)`,
342+
'includeCommandLineAPI': true
343+
}
344+
}, (message) => {
345+
checkException(message);
346+
assert.notStrictEqual(message['result']['value'],
347+
'<inspector console>');
348+
}
349+
],
350+
]);
351+
}
352+
212353
function testWaitsForFrontendDisconnect(session, harness) {
213354
console.log('[test]', 'Verify node waits for the frontend to disconnect');
214355
session.sendInspectorCommands({ 'method': 'Debugger.resume'})
@@ -231,6 +372,7 @@ function runTests(harness) {
231372
testSetBreakpointAndResume,
232373
testInspectScope,
233374
testI18NCharacters,
375+
testCommandLineAPI,
234376
testWaitsForFrontendDisconnect
235377
]).expectShutDown(55);
236378
}

0 commit comments

Comments
 (0)