From 812e351996af7efb413d0962d5ac6600d6cac826 Mon Sep 17 00:00:00 2001 From: Jan Krems Date: Wed, 28 Sep 2016 13:20:54 -0700 Subject: [PATCH 1/3] inspector: Allow require in Runtime.evaluate --- lib/internal/bootstrap_node.js | 19 +++++ src/env.h | 1 + src/inspector_agent.cc | 31 ++++++++ test/inspector/test-inspector.js | 117 +++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index aa428f785d6e8e..b6033092707461 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -282,6 +282,7 @@ return console; } }); + setupInspectorCommandLineAPI(); } function installInspectorConsole(globalConsole) { @@ -310,6 +311,24 @@ return wrappedConsole; } + function setupInspectorCommandLineAPI() { + const inspector = process.binding('inspector'); + const addCommandLineAPIMethod = inspector.addCommandLineAPIMethod; + if (!addCommandLineAPIMethod) return; + + const Module = NativeModule.require('module'); + const path = NativeModule.require('path'); + const cwd = tryGetCwd(path); + + const consoleAPIModule = new Module('[consoleAPI]'); + consoleAPIModule.filename = path.join(cwd, consoleAPIModule.id); + consoleAPIModule.paths = Module._nodeModulePaths(cwd); + + addCommandLineAPIMethod('require', function require(request) { + return consoleAPIModule.require(request); + }); + } + function setupProcessFatal() { const async_wrap = process.binding('async_wrap'); // Arrays containing hook flags and ids for async_hook calls. diff --git a/src/env.h b/src/env.h index f466ecd2ef50fd..abc765c6dc3331 100644 --- a/src/env.h +++ b/src/env.h @@ -292,6 +292,7 @@ namespace node { V(context, v8::Context) \ V(domain_array, v8::Array) \ V(domains_stack_array, v8::Array) \ + V(inspector_console_api_object, v8::Object) \ V(jsstream_constructor_template, v8::FunctionTemplate) \ V(module_load_list_array, v8::Array) \ V(pbkdf2_constructor_template, v8::ObjectTemplate) \ diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 4d51c3d140f941..ce7fcefd3d904b 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -587,6 +587,20 @@ class NodeInspectorClient : public V8InspectorClient { return channel_.get(); } + void installAdditionalCommandLineAPI(v8::Local context, + v8::Local target) { + v8::Local console_api = env_->inspector_console_api_object(); + + v8::Local properties = + console_api->GetOwnPropertyNames(context).ToLocalChecked(); + for (uint32_t i = 0; i < properties->Length(); ++i) { + v8::Local key = properties->Get(context, i).ToLocalChecked(); + target->Set(context, + key, + console_api->Get(context, key).ToLocalChecked()).FromJust(); + } + } + void startRepeatingTimer(double interval_s, TimerCallback callback, void* data) override { @@ -682,6 +696,20 @@ bool Agent::StartIoThread(bool wait_for_connect) { return true; } +static void AddCommandLineAPIMethod( + const v8::FunctionCallbackInfo& info) { + auto env = Environment::GetCurrent(info); + v8::Local context = env->context(); + + if (info.Length() != 2 || !info[0]->IsString() || !info[1]->IsFunction()) { + return env->ThrowTypeError("inspector.addCommandLineAPIMethod takes " + "exactly 2 arguments: a string and a function."); + } + + v8::Local console_api = env->inspector_console_api_object(); + console_api->Set(context, info[0], info[1]).FromJust(); +} + void Agent::Stop() { if (io_ != nullptr) { io_->Stop(); @@ -784,8 +812,11 @@ void Url(const FunctionCallbackInfo& args) { void Agent::InitInspector(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); + env->set_inspector_console_api_object(Object::New(env->isolate())); + Agent* agent = env->inspector_agent(); env->SetMethod(target, "consoleCall", InspectorConsoleCall); + env->SetMethod(target, "addCommandLineAPIMethod", AddCommandLineAPIMethod); if (agent->debug_options_.wait_for_connect()) env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "connect", ConnectJSBindingsSession); diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index 4c8f8f242d7600..b82715c033e632 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -209,6 +209,122 @@ function testI18NCharacters(session) { ]); } +function testCommandLineAPI(session) { + const testModulePath = require.resolve('../fixtures/empty.js'); + const testModuleStr = JSON.stringify(testModulePath); + const printModulePath = require.resolve('../fixtures/printA.js'); + const printModuleStr = JSON.stringify(printModulePath); + session.sendInspectorCommands([ + [ // we can use `require` outside of a callframe with require in scope + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': 'typeof require("fs").readFile === "function"', + 'includeCommandLineAPI': true + } + }, (message) => assert.strictEqual(true, message['result']['value']) + ], + [ // the global require does not have require.cache + { + 'method': 'Runtime.evaluate', 'params': { + 'expression': 'require.cache === undefined', + 'includeCommandLineAPI': true + } + }, (message) => assert.strictEqual(true, message['result']['value']) + ], + [ // the `require` in the module shadows the command line API's `require` + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': 'typeof require.cache', + 'includeCommandLineAPI': true + } + }, (message) => assert.strictEqual('object', message['result']['value']) + ], + [ // `require` twice returns the same value + { + 'method': 'Runtime.evaluate', 'params': { + // 1. We require the same module twice + // 2. We mutate the exports so we can compare it later on + 'expression': ` + Object.assign( + require(${testModuleStr}), + { old: 'yes' } + ) === require(${testModuleStr})`, + 'includeCommandLineAPI': true + } + }, (message) => assert.strictEqual(true, message['result']['value']) + ], + [ // after require the module appears in require.cache + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `require.cache[${testModuleStr}].exports`, + 'generatePreview': true, + 'includeCommandLineAPI': true + } + }, (message) => { + const { properties } = message.result.preview; + assert.strictEqual('old', properties[0].name); + assert.strictEqual('yes', properties[0].value); + } + ], + [ // remove module from require.cache + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `delete require.cache[${testModuleStr}]`, + 'includeCommandLineAPI': true + } + }, + ], + [ // require again, should get fresh (empty) exports + { + 'method': 'Runtime.evaluate', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `require(${testModuleStr})`, + 'generatePreview': true, + 'includeCommandLineAPI': true + } + }, (message) => { + const { properties } = message.result.preview; + assert.strictEqual(0, properties.length); + } + ], + [ // require 2nd module, exports an empty object + { + 'method': 'Runtime.evaluate', 'params': { + // 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `require(${printModuleStr})`, + 'generatePreview': true, + 'includeCommandLineAPI': true + } + }, (message) => { + const { properties } = message.result.preview; + assert.strictEqual(0, properties.length); + } + ], + [ // both modules end up with the same module.parent + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `({ + parentsEqual: + require.cache[${testModuleStr}].parent === + require.cache[${printModuleStr}].parent, + parentId: require.cache[${testModuleStr}].parent.id, + })`, + 'generatePreview': true, + 'includeCommandLineAPI': true + } + }, (message) => { + const { properties } = message.result.preview; + assert.strictEqual('[consoleAPI]', properties[1].value); + assert.strictEqual('true', properties[0].value); + } + ], + ]); +} + function testWaitsForFrontendDisconnect(session, harness) { console.log('[test]', 'Verify node waits for the frontend to disconnect'); session.sendInspectorCommands({ 'method': 'Debugger.resume' }) @@ -231,6 +347,7 @@ function runTests(harness) { testSetBreakpointAndResume, testInspectScope, testI18NCharacters, + testCommandLineAPI, testWaitsForFrontendDisconnect ]).expectShutDown(55); } From 7a4d57b579254084285132fbcd2d9f9880bfc488 Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Tue, 25 Jul 2017 21:44:45 +0800 Subject: [PATCH 2/3] Address comments --- lib/internal/bootstrap_node.js | 16 ++--- src/inspector_agent.cc | 52 +++++++------- test/inspector/test-inspector.js | 113 +++++++++++++++++++------------ 3 files changed, 105 insertions(+), 76 deletions(-) diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index b6033092707461..95a18f33c38236 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -312,21 +312,19 @@ } function setupInspectorCommandLineAPI() { - const inspector = process.binding('inspector'); - const addCommandLineAPIMethod = inspector.addCommandLineAPIMethod; - if (!addCommandLineAPIMethod) return; + const { addCommandLineAPI } = process.binding('inspector'); + if (!addCommandLineAPI) return; const Module = NativeModule.require('module'); + const { makeRequireFunction } = NativeModule.require('internal/module'); const path = NativeModule.require('path'); const cwd = tryGetCwd(path); - const consoleAPIModule = new Module('[consoleAPI]'); - consoleAPIModule.filename = path.join(cwd, consoleAPIModule.id); - consoleAPIModule.paths = Module._nodeModulePaths(cwd); + const consoleAPIModule = new Module(''); + consoleAPIModule.paths = + Module._nodeModulePaths(cwd).concat(Module.globalPaths); - addCommandLineAPIMethod('require', function require(request) { - return consoleAPIModule.require(request); - }); + addCommandLineAPI('require', makeRequireFunction(consoleAPIModule)); } function setupProcessFatal() { diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index ce7fcefd3d904b..bedf74f3b02f61 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -22,6 +22,7 @@ namespace node { namespace inspector { namespace { +using v8::Array; using v8::Context; using v8::External; using v8::Function; @@ -554,6 +555,20 @@ class NodeInspectorClient : public V8InspectorClient { return env_->context(); } + void installAdditionalCommandLineAPI(Local context, + Local target) override { + Local console_api = env_->inspector_console_api_object(); + + Local properties = + console_api->GetOwnPropertyNames(context).ToLocalChecked(); + for (uint32_t i = 0; i < properties->Length(); ++i) { + Local key = properties->Get(context, i).ToLocalChecked(); + target->Set(context, + key, + console_api->Get(context, key).ToLocalChecked()).FromJust(); + } + } + void FatalException(Local error, Local message) { Local context = env_->context(); @@ -587,20 +602,6 @@ class NodeInspectorClient : public V8InspectorClient { return channel_.get(); } - void installAdditionalCommandLineAPI(v8::Local context, - v8::Local target) { - v8::Local console_api = env_->inspector_console_api_object(); - - v8::Local properties = - console_api->GetOwnPropertyNames(context).ToLocalChecked(); - for (uint32_t i = 0; i < properties->Length(); ++i) { - v8::Local key = properties->Get(context, i).ToLocalChecked(); - target->Set(context, - key, - console_api->Get(context, key).ToLocalChecked()).FromJust(); - } - } - void startRepeatingTimer(double interval_s, TimerCallback callback, void* data) override { @@ -696,17 +697,17 @@ bool Agent::StartIoThread(bool wait_for_connect) { return true; } -static void AddCommandLineAPIMethod( - const v8::FunctionCallbackInfo& info) { +static void AddCommandLineAPI( + const FunctionCallbackInfo& info) { auto env = Environment::GetCurrent(info); - v8::Local context = env->context(); + Local context = env->context(); - if (info.Length() != 2 || !info[0]->IsString() || !info[1]->IsFunction()) { - return env->ThrowTypeError("inspector.addCommandLineAPIMethod takes " - "exactly 2 arguments: a string and a function."); + if (info.Length() != 2 || !info[0]->IsString()) { + return env->ThrowTypeError("inspector.addCommandLineAPI takes " + "exactly 2 arguments: a string and a value."); } - v8::Local console_api = env->inspector_console_api_object(); + Local console_api = env->inspector_console_api_object(); console_api->Set(context, info[0], info[1]).FromJust(); } @@ -812,11 +813,16 @@ void Url(const FunctionCallbackInfo& args) { void Agent::InitInspector(Local target, Local unused, Local context, void* priv) { Environment* env = Environment::GetCurrent(context); - env->set_inspector_console_api_object(Object::New(env->isolate())); + { + auto obj = Object::New(env->isolate()); + auto null = Null(env->isolate()); + CHECK(obj->SetPrototype(context, null).FromJust()); + env->set_inspector_console_api_object(obj); + } Agent* agent = env->inspector_agent(); env->SetMethod(target, "consoleCall", InspectorConsoleCall); - env->SetMethod(target, "addCommandLineAPIMethod", AddCommandLineAPIMethod); + env->SetMethod(target, "addCommandLineAPI", AddCommandLineAPI); if (agent->debug_options_.wait_for_connect()) env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "connect", ConnectJSBindingsSession); diff --git a/test/inspector/test-inspector.js b/test/inspector/test-inspector.js index b82715c033e632..f933232553587e 100644 --- a/test/inspector/test-inspector.js +++ b/test/inspector/test-inspector.js @@ -34,6 +34,11 @@ function checkBadPath(err, response) { assert(/WebSockets request was expected/.test(err.response)); } +function checkException(message) { + assert.strictEqual(message['exceptionDetails'], undefined, + 'An exception occurred during execution'); +} + function expectMainScriptSource(result) { const expected = helper.mainScriptSource(); const source = result['scriptSource']; @@ -212,8 +217,10 @@ function testI18NCharacters(session) { function testCommandLineAPI(session) { const testModulePath = require.resolve('../fixtures/empty.js'); const testModuleStr = JSON.stringify(testModulePath); - const printModulePath = require.resolve('../fixtures/printA.js'); - const printModuleStr = JSON.stringify(printModulePath); + const printAModulePath = require.resolve('../fixtures/printA.js'); + const printAModuleStr = JSON.stringify(printAModulePath); + const printBModulePath = require.resolve('../fixtures/printB.js'); + const printBModuleStr = JSON.stringify(printBModulePath); session.sendInspectorCommands([ [ // we can use `require` outside of a callframe with require in scope { @@ -221,24 +228,25 @@ function testCommandLineAPI(session) { 'expression': 'typeof require("fs").readFile === "function"', 'includeCommandLineAPI': true } - }, (message) => assert.strictEqual(true, message['result']['value']) + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } ], - [ // the global require does not have require.cache + [ // the global require has the same properties as a normal `require` { 'method': 'Runtime.evaluate', 'params': { - 'expression': 'require.cache === undefined', - 'includeCommandLineAPI': true - } - }, (message) => assert.strictEqual(true, message['result']['value']) - ], - [ // the `require` in the module shadows the command line API's `require` - { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': 'typeof require.cache', + 'expression': [ + 'typeof require.resolve === "function"', + 'typeof require.extensions === "object"', + 'typeof require.cache === "object"' + ].join(' && '), 'includeCommandLineAPI': true } - }, (message) => assert.strictEqual('object', message['result']['value']) + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } ], [ // `require` twice returns the same value { @@ -252,74 +260,91 @@ function testCommandLineAPI(session) { ) === require(${testModuleStr})`, 'includeCommandLineAPI': true } - }, (message) => assert.strictEqual(true, message['result']['value']) + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } ], [ // after require the module appears in require.cache { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `require.cache[${testModuleStr}].exports`, - 'generatePreview': true, + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify( + require.cache[${testModuleStr}].exports + )`, 'includeCommandLineAPI': true } }, (message) => { - const { properties } = message.result.preview; - assert.strictEqual('old', properties[0].name); - assert.strictEqual('yes', properties[0].value); + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), + { old: 'yes' }); } ], [ // remove module from require.cache { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'method': 'Runtime.evaluate', 'params': { 'expression': `delete require.cache[${testModuleStr}]`, 'includeCommandLineAPI': true } - }, + }, (message) => { + checkException(message); + assert.strictEqual(message['result']['value'], true); + } ], [ // require again, should get fresh (empty) exports { 'method': 'Runtime.evaluate', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `require(${testModuleStr})`, - 'generatePreview': true, + 'expression': `JSON.stringify(require(${testModuleStr}))`, 'includeCommandLineAPI': true } }, (message) => { - const { properties } = message.result.preview; - assert.strictEqual(0, properties.length); + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); } ], [ // require 2nd module, exports an empty object { 'method': 'Runtime.evaluate', 'params': { - // 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `require(${printModuleStr})`, - 'generatePreview': true, + 'expression': `JSON.stringify(require(${printAModuleStr}))`, 'includeCommandLineAPI': true } }, (message) => { - const { properties } = message.result.preview; - assert.strictEqual(0, properties.length); + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), {}); } ], [ // both modules end up with the same module.parent { - 'method': 'Debugger.evaluateOnCallFrame', 'params': { - 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', - 'expression': `({ + 'method': 'Runtime.evaluate', 'params': { + 'expression': `JSON.stringify({ parentsEqual: require.cache[${testModuleStr}].parent === - require.cache[${printModuleStr}].parent, + require.cache[${printAModuleStr}].parent, parentId: require.cache[${testModuleStr}].parent.id, })`, - 'generatePreview': true, 'includeCommandLineAPI': true } }, (message) => { - const { properties } = message.result.preview; - assert.strictEqual('[consoleAPI]', properties[1].value); - assert.strictEqual('true', properties[0].value); + checkException(message); + assert.deepStrictEqual(JSON.parse(message['result']['value']), { + parentsEqual: true, + parentId: '' + }); + } + ], + [ // the `require` in the module shadows the command line API's `require` + { + 'method': 'Debugger.evaluateOnCallFrame', 'params': { + 'callFrameId': '{"ordinal":0,"injectedScriptId":1}', + 'expression': `( + require(${printBModuleStr}), + require.cache[${printBModuleStr}].parent.id + )`, + 'includeCommandLineAPI': true + } + }, (message) => { + checkException(message); + assert.notStrictEqual(message['result']['value'], + ''); } ], ]); From 0a183cd5c58b4c13446702118bbb3cba9a5db6af Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Mon, 31 Jul 2017 08:21:58 +0800 Subject: [PATCH 3/3] Fix indentation --- lib/internal/bootstrap_node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/bootstrap_node.js b/lib/internal/bootstrap_node.js index 95a18f33c38236..cf517cdcf2b5ef 100644 --- a/lib/internal/bootstrap_node.js +++ b/lib/internal/bootstrap_node.js @@ -322,7 +322,7 @@ const consoleAPIModule = new Module(''); consoleAPIModule.paths = - Module._nodeModulePaths(cwd).concat(Module.globalPaths); + Module._nodeModulePaths(cwd).concat(Module.globalPaths); addCommandLineAPI('require', makeRequireFunction(consoleAPIModule)); }