From 812e351996af7efb413d0962d5ac6600d6cac826 Mon Sep 17 00:00:00 2001 From: Jan Krems Date: Wed, 28 Sep 2016 13:20:54 -0700 Subject: [PATCH] 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); }