diff --git a/doc/api/inspector.md b/doc/api/inspector.md index 00fc927d0d35ee..5ffa3332302493 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -10,6 +10,45 @@ It can be accessed using: const inspector = require('inspector'); ``` +## inspector.attachContext(contextifiedSandbox[, options]) + +* `contextifiedSandbox` {Object} The [contextified][] object to attach to the + inspector. +* `options` {Object} + * `name` {string} Name of the context. If not specified, a default name `vm + Module Context ${idx}` where `idx` is the incrementing index corresponding + to this call will be used. + * `origin` {string} URL of the webpage this context corresponds to. Defaults + to `false`. + + +Make inspector aware of the specified context. This allows code running in that +context to be debugged, including support for [`debugger` statement][]. If the +context is already being tracked by inspector, this function is a no-op. + +*Note:* Calling this function will prevent `contextifiedSandbox` from being +garbage collected. To prevent memory leaks, [`inspector.detachContext()`][] +*must* be called after work on the context has been completed. + +## inspector.contextAttached(contextifiedSandbox) + +* `contextifiedSandbox` {Object} A [contextified][] object +* Returns: {boolean} + +Check if the specified context is attached to V8 inspector. + +## inspector.detachContext(contextifiedSandbox) + +* `contextifiedSandbox` {Object} The [contextified][] object to detach from + inspector + +Inform the V8 inspector that the context has been destroyed. If the context is +not being tracked by inspector, this function is a no-op. + ## inspector.open([port[, host[, wait]]]) * port {number} Port to listen on for inspector connections. Optional, @@ -135,6 +174,8 @@ messages again. Reconnected session will lose all inspector state, such as enabled agents or configured breakpoints. +[contextified]: vm.html#vm_what_does_it_mean_to_contextify_an_object +[`debugger` statement]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger [`session.connect()`]: #inspector_session_connect [`Debugger.paused`]: https://chromedevtools.github.io/devtools-protocol/v8/Debugger/#event-paused [`EventEmitter`]: events.html#events_class_eventemitter diff --git a/doc/api/vm.md b/doc/api/vm.md index 42046e01191b51..44924317232ff7 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -59,6 +59,11 @@ changes: `cachedData` property of the returned `vm.Script` instance. The `cachedDataProduced` value will be set to either `true` or `false` depending on whether code cache data is produced successfully. + * `contextifiedSandbox` {Object} A [contextified][] sandbox to associate with + the current script. This does not bind the created script to this context + (i.e. `runInContext` still works with all contexts), but merely informs V8 + (and V8 inspector) that the script is associated with the context. Defaults + to the current context. Creating a new `vm.Script` object compiles `code` but does not run it. The compiled `vm.Script` can be run later multiple times. It is important to note @@ -94,12 +99,28 @@ changes: event that have been attached via `process.on("SIGINT")` will be disabled during script execution, but will continue to work after that. If execution is terminated, an [`Error`][] will be thrown. + * `doNotInformInspector` {boolean} If `true`, do not try to attach the + context to inspector. Debugging with `--inspect` will not work for code in + the context in that case. Runs the compiled code contained by the `vm.Script` object within the given `contextifiedSandbox` and returns the result. Running code does not have access to local scope. +By default, this function checks if inspector was already aware of the context +(if support for inspector is available in the Node.js build). If not, and if +`doNotInformInspector` is not specified, this function attaches it to inspector +using [`inspector.attachContext()`][], and detaches it before returning. +However, *it is still recommended that you handle context attachment and +detachment manually,* since: + +1. Asynchronous code running in the context would still not have inspector + debugging support, if relying on automatic attachment provided by this + function. +2. Attaching and detaching per `runInContext()` run can have a significant + performance cost if this is done for the same context again and again. + The following example compiles code that increments a global variable, sets the value of another global variable, then execute the code multiple times. The globals are contained in the `sandbox` object. @@ -149,11 +170,23 @@ added: v0.3.1 * `timeout` {number} Specifies the number of milliseconds to execute `code` before terminating execution. If execution is terminated, an [`Error`][] will be thrown. + * `doNotInformInspector` {boolean} If `true`, do not try to attach the + context to inspector. Debugging with `--inspect` will not work for code in + the context in that case. First contextifies the given `sandbox`, runs the compiled code contained by the `vm.Script` object within the created sandbox, and returns the result. Running code does not have access to local scope. +By default, if support for inspector is available in the Node.js build, and if +`doNotInformInspector` is not specified, this function attaches the new context +to inspector using [`inspector.attachContext()`][], and detaches it before +returning. However, if debugging supported is desired, *it is recommended that +you handle context creation, attachment to inspector, and detachment from +inspector manually,* since asynchronous code running in the context would still +not have inspector debugging support, if relying on automatic attachment +provided by this function. + The following example compiles code that sets a global variable, then executes the code multiple times in different contexts. The globals are set on and contained within each individual `sandbox`. @@ -284,12 +317,28 @@ Returns `true` if the given `sandbox` object has been [contextified][] using * `timeout` {number} Specifies the number of milliseconds to execute `code` before terminating execution. If execution is terminated, an [`Error`][] will be thrown. + * `doNotInformInspector` {boolean} If `true`, do not try to attach the + context to inspector. Debugging with `--inspect` will not work for code in + the context in that case. The `vm.runInContext()` method compiles `code`, runs it within the context of the `contextifiedSandbox`, then returns the result. Running code does not have access to the local scope. The `contextifiedSandbox` object *must* have been previously [contextified][] using the [`vm.createContext()`][] method. +By default, this function checks if inspector was already aware of the context +(if support for inspector is available in the Node.js build). If not, and if +`doNotInformInspector` is not specified, this function attaches it to inspector +using [`inspector.attachContext()`][], and detaches it before returning. +However, *it is still recommended that you handle context attachment and +detachment manually,* since: + +1. Asynchronous code running in the context would still not have inspector + debugging support, if relying on automatic attachment provided by this + function. +2. Attaching and detaching per `runInContext()` run can have a significant + performance cost if this is done for the same context again and again. + The following example compiles and executes different scripts using a single [contextified][] object: diff --git a/lib/inspector.js b/lib/inspector.js index 6a80c36d528a1d..34a24056c61781 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -1,8 +1,17 @@ 'use strict'; +const errors = require('internal/errors'); const EventEmitter = require('events'); const util = require('util'); -const { connect, open, url } = process.binding('inspector'); +const { isContext } = process.binding('contextify'); +const { + connect, + open, + url, + contextAttached: _contextAttached, + attachContext: _attachContext, + detachContext: _detachContext +} = process.binding('inspector'); if (!connect) throw new Error('Inspector is not available'); @@ -86,9 +95,49 @@ class Session extends EventEmitter { } } +function checkSandbox(sandbox) { + if (typeof sandbox !== 'object') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'sandbox', + 'object', sandbox); + } + if (!isContext(sandbox)) { + throw new errors.TypeError('ERR_SANDBOX_NOT_CONTEXTIFIED'); + } +} + +let ctxIdx = 1; +function attachContext(contextifiedSandbox, { + name = `vm Module Context ${ctxIdx++}`, + origin +} = {}) { + checkSandbox(contextifiedSandbox); + if (typeof name !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options.name', + 'string', name); + } + if (origin !== undefined && typeof origin !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options.origin', + 'string', origin); + } + _attachContext(contextifiedSandbox, name, origin); +} + +function detachContext(contextifiedSandbox) { + checkSandbox(contextifiedSandbox); + _detachContext(contextifiedSandbox); +} + +function contextAttached(contextifiedSandbox) { + checkSandbox(contextifiedSandbox); + return _contextAttached(contextifiedSandbox); +} + module.exports = { open: (port, host, wait) => open(port, host, !!wait), close: process._debugEnd, url: url, - Session + Session, + attachContext, + detachContext, + contextAttached }; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 435db9e2bfd5e6..bf51213c7e1dd0 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -165,6 +165,8 @@ E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function'); E('ERR_NAPI_CONS_PROTOTYPE_OBJECT', 'Constructor.prototype must be an object'); E('ERR_NO_CRYPTO', 'Node.js is not compiled with OpenSSL crypto support'); E('ERR_PARSE_HISTORY_DATA', 'Could not parse history data in %s'); +E('ERR_SANDBOX_NOT_CONTEXTIFIED', + 'Provided sandbox must have been converted to a context'); E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound'); E('ERR_SOCKET_BAD_TYPE', 'Bad socket type specified. Valid types are: udp4, udp6'); diff --git a/lib/vm.js b/lib/vm.js index 5bee450becec8b..1eb17535cce7f7 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -23,6 +23,13 @@ const binding = process.binding('contextify'); const Script = binding.ContextifyScript; +const { contextAttached, attachContext, detachContext } = (() => { + try { + return require('inspector'); + } catch (err) { + return {}; + } +})(); // The binding provides a few useful primitives: // - Script(code, { filename = "evalmachine.anonymous", @@ -46,11 +53,23 @@ Script.prototype.runInThisContext = function(options) { }; Script.prototype.runInContext = function(contextifiedSandbox, options) { - if (options && options.breakOnSigint && process._events.SIGINT) { - return sigintHandlersWrap(realRunInContext, this, - [contextifiedSandbox, options]); - } else { - return realRunInContext.call(this, contextifiedSandbox, options); + const needToAttach = contextAttached && + !(options && options.doNotInformInspector) && + !contextAttached(contextifiedSandbox); + if (needToAttach) { + attachContext(contextifiedSandbox); + } + try { + if (options && options.breakOnSigint && process._events.SIGINT) { + return sigintHandlersWrap(realRunInContext, this, + [contextifiedSandbox, options]); + } else { + return realRunInContext.call(this, contextifiedSandbox, options); + } + } finally { + if (needToAttach) { + detachContext(contextifiedSandbox); + } } }; @@ -104,12 +123,32 @@ function runInDebugContext(code) { } function runInContext(code, contextifiedSandbox, options) { + options = Object.assign({}, options, { + contextifiedSandbox + }); return createScript(code, options) .runInContext(contextifiedSandbox, options); } function runInNewContext(code, sandbox, options) { - return createScript(code, options).runInNewContext(sandbox, options); + sandbox = createContext(sandbox); + const needToAttach = contextAttached && + !(options && options.doNotInformInspector) && + !contextAttached(sandbox); + if (needToAttach) { + attachContext(sandbox); + } + try { + options = Object.assign({}, options, { + contextifiedSandbox: sandbox, + doNotInformInspector: true + }); + return createScript(code, options).runInNewContext(sandbox, options); + } finally { + if (needToAttach) { + detachContext(sandbox); + } + } } function runInThisContext(code, options) { diff --git a/node.gyp b/node.gyp index b0e4676a96e477..d001e7246b4912 100644 --- a/node.gyp +++ b/node.gyp @@ -175,6 +175,7 @@ 'src/node_config.cc', 'src/node_constants.cc', 'src/node_contextify.cc', + 'src/node_contextify.h', 'src/node_debug_options.cc', 'src/node_file.cc', 'src/node_http_parser.cc', @@ -624,8 +625,10 @@ '<(OBJ_PATH)<(OBJ_SEPARATOR)env.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_buffer.<(OBJ_SUFFIX)', + '<(OBJ_PATH)<(OBJ_SEPARATOR)node_contextify.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_i18n.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)node_url.<(OBJ_SUFFIX)', + '<(OBJ_PATH)<(OBJ_SEPARATOR)node_watchdog.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)util.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)string_bytes.<(OBJ_SUFFIX)', '<(OBJ_PATH)<(OBJ_SEPARATOR)string_search.<(OBJ_SUFFIX)', diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index a06ff032ff7d51..719ea47c68bb14 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -4,6 +4,7 @@ #include "env.h" #include "env-inl.h" #include "node.h" +#include "node_contextify.h" #include "v8-inspector.h" #include "v8-platform.h" #include "util.h" @@ -11,6 +12,7 @@ #include "libplatform/libplatform.h" +#include #include #include @@ -21,6 +23,9 @@ namespace node { namespace inspector { namespace { + +using node::contextify::ContextifyContext; + using v8::Context; using v8::External; using v8::Function; @@ -43,6 +48,10 @@ using v8_inspector::V8Inspector; static uv_sem_t start_io_thread_semaphore; static uv_async_t start_io_thread_async; +// Used in NodeInspectorClient::currentTimeMS() below. +const int NANOS_PER_MSEC = 1000000; +const int CONTEXT_GROUP_ID = 1; + class StartIoTask : public v8::Task { public: explicit StartIoTask(Agent* agent) : agent(agent) {} @@ -376,9 +385,84 @@ void CallAndPauseOnStart( } } -// Used in NodeInspectorClient::currentTimeMS() below. -const int NANOS_PER_MSEC = 1000000; -const int CONTEXT_GROUP_ID = 1; +void AttachContext(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args[0]->IsObject()) { + env->ThrowTypeError("sandbox must be an object"); + return; + } + Local sandbox = args[0].As(); + ContextifyContext* contextify_context = + ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); + if (contextify_context == nullptr) { + return env->ThrowTypeError( + "sandbox argument must have been converted to a context."); + } + + if (contextify_context->context().IsEmpty()) + return; + + const char* name = + args[1]->IsString() ? + Utf8Value(env->isolate(), args[1]).out() : + "vm Module Context"; + const char* origin = + args[2]->IsString() ? + Utf8Value(env->isolate(), args[2]).out() : + nullptr; + + // TODO(TimothyGu): Don't allow customizing group ID for now; not sure what + // it's used for. + int group_id = CONTEXT_GROUP_ID; + + auto info = new node::inspector::ContextInfo( + contextify_context->context(), group_id, name, origin, + "{\"isDefault\":false}"); + // Ignore error. + env->inspector_agent()->ContextCreated(info); +} + +void ContextAttached(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args[0]->IsObject()) { + env->ThrowTypeError("sandbox must be an object"); + return; + } + Local sandbox = args[0].As(); + ContextifyContext* contextify_context = + ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); + if (contextify_context == nullptr) { + return env->ThrowTypeError( + "sandbox argument must have been converted to a context."); + } + + if (contextify_context->context().IsEmpty()) + return; + + args.GetReturnValue().Set( + env->inspector_agent()->ContextRegistered( + contextify_context->context())); +} + +void DetachContext(const v8::FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args[0]->IsObject()) { + env->ThrowTypeError("sandbox must be an object"); + return; + } + Local sandbox = args[0].As(); + ContextifyContext* contextify_context = + ContextifyContext::ContextFromContextifiedSandbox(env, sandbox); + if (contextify_context == nullptr) { + return env->ThrowTypeError( + "sandbox argument must have been converted to a context."); + } + + if (contextify_context->context().IsEmpty()) + return; + + env->inspector_agent()->ContextDestroyed(contextify_context->context()); +} class ChannelImpl final : public v8_inspector::V8Inspector::Channel { public: @@ -459,11 +543,24 @@ class NodeInspectorClient : public v8_inspector::V8InspectorClient { return uv_hrtime() * 1.0 / NANOS_PER_MSEC; } - void contextCreated(Local context, const std::string& name) { - std::unique_ptr name_buffer = Utf8ToStringView(name); - v8_inspector::V8ContextInfo info(context, CONTEXT_GROUP_ID, - name_buffer->string()); - client_->contextCreated(info); + void contextCreated(const node::inspector::ContextInfo* info) { + std::unique_ptr name_buffer = Utf8ToStringView(info->name()); + v8_inspector::V8ContextInfo v8_info(info->context(env_->isolate()), + info->group_id(), + name_buffer->string()); + + std::unique_ptr origin_buffer; + std::unique_ptr aux_data_buffer; + if (info->origin() != nullptr) { + origin_buffer = Utf8ToStringView(info->origin()); + v8_info.origin = origin_buffer->string(); + } + if (info->aux_data() != nullptr) { + aux_data_buffer = Utf8ToStringView(info->aux_data()); + v8_info.auxData = aux_data_buffer->string(); + } + + client_->contextCreated(v8_info); } void contextDestroyed(Local context) { @@ -546,6 +643,39 @@ Agent::Agent(Environment* env) : parent_env_(env), Agent::~Agent() { } +bool Agent::ContextRegistered(Local context) { + auto it = std::find_if( + contexts_.begin(), contexts_.end(), + [&] (const node::inspector::ContextInfo*& info) { + return info->context(parent_env_->isolate()) == context; + }); + return it != contexts_.end(); +} + +bool Agent::ContextCreated(const node::inspector::ContextInfo* info) { + auto isolate = parent_env_->isolate(); + if (ContextRegistered(info->context(isolate))) { + return false; + } + contexts_.push_back(info); + client_->contextCreated(info); + return true; +} + +void Agent::ContextDestroyed(Local context) { + auto it = std::find_if( + contexts_.begin(), contexts_.end(), + [&] (const node::inspector::ContextInfo*& info) { + return info->context(parent_env_->isolate()) == context; + }); + if (it == contexts_.end()) { + return; + } + delete *it; + contexts_.erase(it); + client_->contextDestroyed(context); +} + bool Agent::Start(v8::Platform* platform, const char* path, const DebugOptions& options) { path_ = path == nullptr ? "" : path; @@ -553,7 +683,10 @@ bool Agent::Start(v8::Platform* platform, const char* path, client_ = std::unique_ptr( new NodeInspectorClient(parent_env_, platform)); - client_->contextCreated(parent_env_->context(), "Node.js Main Context"); + CHECK(ContextCreated( + new node::inspector::ContextInfo( + parent_env_->context(), CONTEXT_GROUP_ID, "Node.js Main Context", + nullptr, "{\"isDefault\":true}"))); platform_ = platform; CHECK_EQ(0, uv_async_init(uv_default_loop(), &start_io_thread_async, @@ -627,7 +760,9 @@ bool Agent::IsConnected() { void Agent::WaitForDisconnect() { CHECK_NE(client_, nullptr); - client_->contextDestroyed(parent_env_->context()); + for (const node::inspector::ContextInfo*& info : contexts_) { + ContextDestroyed(info->context(parent_env_->isolate())); + } if (io_ != nullptr) { io_->WaitForDisconnect(); } @@ -713,6 +848,9 @@ void Agent::InitInspector(Local target, Local unused, Environment* env = Environment::GetCurrent(context); Agent* agent = env->inspector_agent(); env->SetMethod(target, "consoleCall", InspectorConsoleCall); + env->SetMethod(target, "contextAttached", ContextAttached); + env->SetMethod(target, "attachContext", AttachContext); + env->SetMethod(target, "detachContext", DetachContext); if (agent->debug_options_.wait_for_connect()) env->SetMethod(target, "callAndPauseOnStart", CallAndPauseOnStart); env->SetMethod(target, "connect", ConnectJSBindingsSession); diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 80967212cd7aef..5cf545f7f03ffc 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -2,6 +2,7 @@ #define SRC_INSPECTOR_AGENT_H_ #include +#include #include @@ -9,6 +10,7 @@ #error("This header can only be used when inspector is enabled") #endif +#include "v8.h" #include "node_debug_options.h" // Forward declaration to break recursive dependency chain with src/env.h. @@ -46,6 +48,35 @@ class InspectorSessionDelegate { class InspectorIo; class NodeInspectorClient; +class ContextInfo { + public: + explicit ContextInfo(v8::Local context, const int group_id, + const char* name, const char* origin = nullptr, + const char* aux_data = nullptr) + : group_id_(group_id), + name_(name), + origin_(origin), + aux_data_(aux_data) { + context_.Reset(context->GetIsolate(), context); + } + + inline v8::Local context(v8::Isolate* isolate) const { + return context_.Get(isolate); + } + + int group_id() const { return group_id_; } + const char* name() const { return name_; } + const char* origin() const { return origin_; } + const char* aux_data() const { return aux_data_; } + + private: + v8::Persistent context_; + const int group_id_; + const char* name_; + const char* origin_; + const char* aux_data_; +}; + class Agent { public: explicit Agent(node::Environment* env); @@ -57,6 +88,10 @@ class Agent { // Stop and destroy io_ void Stop(); + bool ContextRegistered(v8::Local context); + bool ContextCreated(const node::inspector::ContextInfo* info); + void ContextDestroyed(v8::Local context); + bool IsStarted() { return !!client_; } // IO thread started, and client connected @@ -102,6 +137,7 @@ class Agent { std::unique_ptr client_; std::unique_ptr io_; v8::Platform* platform_; + std::vector contexts_; bool enabled_; std::string path_; DebugOptions debug_options_; diff --git a/src/inspector_socket_server.cc b/src/inspector_socket_server.cc index cdc907ee9b263b..c3663bfe626e74 100644 --- a/src/inspector_socket_server.cc +++ b/src/inspector_socket_server.cc @@ -9,6 +9,8 @@ #include #include +#define DEBUG_TRANSACTION 0 + namespace node { namespace inspector { @@ -566,8 +568,11 @@ void SocketSession::ReadCallback(uv_stream_t* stream, ssize_t read, InspectorSocket* socket = inspector_from_stream(stream); SocketSession* session = SocketSession::From(socket); if (read > 0) { - session->server_->MessageReceived(session->id_, - std::string(buf->base, read)); + std::string str(buf->base, read); +#if DEBUG_TRANSACTION + printf(">>> %s\n", str.c_str()); +#endif // DEBUG_TRANSACTION + session->server_->MessageReceived(session->id_, str); } else { session->Close(); } @@ -576,6 +581,9 @@ void SocketSession::ReadCallback(uv_stream_t* stream, ssize_t read, } void SocketSession::Send(const std::string& message) { +#if DEBUG_TRANSACTION + printf("<<< %s\n", message.c_str()); +#endif // DEBUG_TRANSACTION inspector_write(&socket_, message.data(), message.length()); } diff --git a/src/node_contextify.cc b/src/node_contextify.cc index 792b1c4f8009f9..137e0e4a5715bb 100644 --- a/src/node_contextify.cc +++ b/src/node_contextify.cc @@ -19,6 +19,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "node_contextify.h" #include "node.h" #include "node_internals.h" #include "node_watchdog.h" @@ -67,451 +68,432 @@ using v8::UnboundScript; using v8::Value; using v8::WeakCallbackInfo; -namespace { +namespace contextify { -class ContextifyContext { - protected: - // V8 reserves the first field in context objects for the debugger. We use the - // second field to hold a reference to the sandbox object. - enum { kSandboxObjectIndex = 1 }; +ContextifyContext::ContextifyContext(Environment* env, Local sandbox_obj) : env_(env) { + Local v8_context = CreateV8Context(env, sandbox_obj); + context_.Reset(env->isolate(), v8_context); - Environment* const env_; - Persistent context_; + // Allocation failure or maximum call stack size reached + if (context_.IsEmpty()) + return; + context_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); + context_.MarkIndependent(); +} - public: - ContextifyContext(Environment* env, Local sandbox_obj) : env_(env) { - Local v8_context = CreateV8Context(env, sandbox_obj); - context_.Reset(env->isolate(), v8_context); - - // Allocation failure or maximum call stack size reached - if (context_.IsEmpty()) - return; - context_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter); - context_.MarkIndependent(); - } +ContextifyContext::~ContextifyContext() { + context_.Reset(); +} - ~ContextifyContext() { - context_.Reset(); - } +// XXX(isaacs): This function only exists because of a shortcoming of +// the V8 SetNamedPropertyHandler function. +// +// It does not provide a way to intercept Object.defineProperty(..) +// calls. As a result, these properties are not copied onto the +// contextified sandbox when a new global property is added via either +// a function declaration or a Object.defineProperty(global, ...) call. +// +// Note that any function declarations or Object.defineProperty() +// globals that are created asynchronously (in a setTimeout, callback, +// etc.) will happen AFTER the call to copy properties, and thus not be +// caught. +// +// The way to properly fix this is to add some sort of a +// Object::SetNamedDefinePropertyHandler() function that takes a callback, +// which receives the property name and property descriptor as arguments. +// +// Luckily, such situations are rare, and asynchronously-added globals +// weren't supported by Node's VM module until 0.12 anyway. But, this +// should be fixed properly in V8, and this copy function should be +// removed once there is a better way. +void ContextifyContext::CopyProperties() { + HandleScope scope(env()->isolate()); + + Local context = PersistentToLocal(env()->isolate(), context_); + Local global = + context->Global()->GetPrototype()->ToObject(env()->isolate()); + Local sandbox_obj = sandbox(); + + Local clone_property_method; + + Local names = global->GetOwnPropertyNames(); + int length = names->Length(); + for (int i = 0; i < length; i++) { + Local key = names->Get(i)->ToString(env()->isolate()); + Maybe has = sandbox_obj->HasOwnProperty(context, key); + + // Check for pending exceptions + if (has.IsNothing()) + return; - inline Environment* env() const { - return env_; + if (!has.FromJust()) { + Local desc_vm_context = + global->GetOwnPropertyDescriptor(context, key) + .ToLocalChecked().As(); + + bool is_accessor = + desc_vm_context->Has(context, env()->get_string()).FromJust() || + desc_vm_context->Has(context, env()->set_string()).FromJust(); + + auto define_property_on_sandbox = [&] (PropertyDescriptor* desc) { + desc->set_configurable(desc_vm_context + ->Get(context, env()->configurable_string()).ToLocalChecked() + ->BooleanValue(context).FromJust()); + desc->set_enumerable(desc_vm_context + ->Get(context, env()->enumerable_string()).ToLocalChecked() + ->BooleanValue(context).FromJust()); + CHECK(sandbox_obj->DefineProperty(context, key, *desc).FromJust()); + }; + + if (is_accessor) { + Local get = + desc_vm_context->Get(context, env()->get_string()) + .ToLocalChecked().As(); + Local set = + desc_vm_context->Get(context, env()->set_string()) + .ToLocalChecked().As(); + + PropertyDescriptor desc(get, set); + define_property_on_sandbox(&desc); + } else { + Local value = + desc_vm_context->Get(context, env()->value_string()) + .ToLocalChecked(); + + bool writable = + desc_vm_context->Get(context, env()->writable_string()) + .ToLocalChecked()->BooleanValue(context).FromJust(); + + PropertyDescriptor desc(value, writable); + define_property_on_sandbox(&desc); + } + } } +} - inline Local context() const { - return PersistentToLocal(env()->isolate(), context_); - } +// This is an object that just keeps an internal pointer to this +// ContextifyContext. It's passed to the NamedPropertyHandler. If we +// pass the main JavaScript context object we're embedded in, then the +// NamedPropertyHandler will store a reference to it forever and keep it +// from getting gc'd. +Local ContextifyContext::CreateDataWrapper(Environment* env) { + EscapableHandleScope scope(env->isolate()); + Local wrapper = + env->script_data_constructor_function() + ->NewInstance(env->context()).FromMaybe(Local()); + if (wrapper.IsEmpty()) + return scope.Escape(Local::New(env->isolate(), Local())); + + Wrap(wrapper, this); + return scope.Escape(wrapper); +} - inline Local global_proxy() const { - return context()->Global(); - } +Local ContextifyContext::CreateV8Context(Environment* env, Local sandbox_obj) { + EscapableHandleScope scope(env->isolate()); + Local function_template = + FunctionTemplate::New(env->isolate()); + function_template->SetHiddenPrototype(true); + function_template->SetClassName(sandbox_obj->GetConstructorName()); - inline Local sandbox() const { - return Local::Cast(context()->GetEmbedderData(kSandboxObjectIndex)); - } + Local object_template = + function_template->InstanceTemplate(); - // XXX(isaacs): This function only exists because of a shortcoming of - // the V8 SetNamedPropertyHandler function. - // - // It does not provide a way to intercept Object.defineProperty(..) - // calls. As a result, these properties are not copied onto the - // contextified sandbox when a new global property is added via either - // a function declaration or a Object.defineProperty(global, ...) call. - // - // Note that any function declarations or Object.defineProperty() - // globals that are created asynchronously (in a setTimeout, callback, - // etc.) will happen AFTER the call to copy properties, and thus not be - // caught. - // - // The way to properly fix this is to add some sort of a - // Object::SetNamedDefinePropertyHandler() function that takes a callback, - // which receives the property name and property descriptor as arguments. - // - // Luckily, such situations are rare, and asynchronously-added globals - // weren't supported by Node's VM module until 0.12 anyway. But, this - // should be fixed properly in V8, and this copy function should be - // removed once there is a better way. - void CopyProperties() { - HandleScope scope(env()->isolate()); - - Local context = PersistentToLocal(env()->isolate(), context_); - Local global = - context->Global()->GetPrototype()->ToObject(env()->isolate()); - Local sandbox_obj = sandbox(); - - Local clone_property_method; - - Local names = global->GetOwnPropertyNames(); - int length = names->Length(); - for (int i = 0; i < length; i++) { - Local key = names->Get(i)->ToString(env()->isolate()); - Maybe has = sandbox_obj->HasOwnProperty(context, key); - - // Check for pending exceptions - if (has.IsNothing()) - return; + NamedPropertyHandlerConfiguration config(GlobalPropertyGetterCallback, + GlobalPropertySetterCallback, + GlobalPropertyQueryCallback, + GlobalPropertyDeleterCallback, + GlobalPropertyEnumeratorCallback, + CreateDataWrapper(env)); + object_template->SetHandler(config); - if (!has.FromJust()) { - Local desc_vm_context = - global->GetOwnPropertyDescriptor(context, key) - .ToLocalChecked().As(); - - bool is_accessor = - desc_vm_context->Has(context, env()->get_string()).FromJust() || - desc_vm_context->Has(context, env()->set_string()).FromJust(); - - auto define_property_on_sandbox = [&] (PropertyDescriptor* desc) { - desc->set_configurable(desc_vm_context - ->Get(context, env()->configurable_string()).ToLocalChecked() - ->BooleanValue(context).FromJust()); - desc->set_enumerable(desc_vm_context - ->Get(context, env()->enumerable_string()).ToLocalChecked() - ->BooleanValue(context).FromJust()); - CHECK(sandbox_obj->DefineProperty(context, key, *desc).FromJust()); - }; - - if (is_accessor) { - Local get = - desc_vm_context->Get(context, env()->get_string()) - .ToLocalChecked().As(); - Local set = - desc_vm_context->Get(context, env()->set_string()) - .ToLocalChecked().As(); - - PropertyDescriptor desc(get, set); - define_property_on_sandbox(&desc); - } else { - Local value = - desc_vm_context->Get(context, env()->value_string()) - .ToLocalChecked(); - - bool writable = - desc_vm_context->Get(context, env()->writable_string()) - .ToLocalChecked()->BooleanValue(context).FromJust(); - - PropertyDescriptor desc(value, writable); - define_property_on_sandbox(&desc); - } - } - } -} + Local ctx = Context::New(env->isolate(), nullptr, object_template); - - // This is an object that just keeps an internal pointer to this - // ContextifyContext. It's passed to the NamedPropertyHandler. If we - // pass the main JavaScript context object we're embedded in, then the - // NamedPropertyHandler will store a reference to it forever and keep it - // from getting gc'd. - Local CreateDataWrapper(Environment* env) { - EscapableHandleScope scope(env->isolate()); - Local wrapper = - env->script_data_constructor_function() - ->NewInstance(env->context()).FromMaybe(Local()); - if (wrapper.IsEmpty()) - return scope.Escape(Local::New(env->isolate(), Local())); - - Wrap(wrapper, this); - return scope.Escape(wrapper); + if (ctx.IsEmpty()) { + env->ThrowError("Could not instantiate context"); + return Local(); } + ctx->SetSecurityToken(env->context()->GetSecurityToken()); - Local CreateV8Context(Environment* env, Local sandbox_obj) { - EscapableHandleScope scope(env->isolate()); - Local function_template = - FunctionTemplate::New(env->isolate()); - function_template->SetHiddenPrototype(true); - - function_template->SetClassName(sandbox_obj->GetConstructorName()); - - Local object_template = - function_template->InstanceTemplate(); + // We need to tie the lifetime of the sandbox object with the lifetime of + // newly created context. We do this by making them hold references to each + // other. The context can directly hold a reference to the sandbox as an + // embedder data field. However, we cannot hold a reference to a v8::Context + // directly in an Object, we instead hold onto the new context's global + // object instead (which then has a reference to the context). + ctx->SetEmbedderData(kSandboxObjectIndex, sandbox_obj); + sandbox_obj->SetPrivate(env->context(), + env->contextify_global_private_symbol(), + ctx->Global()); - NamedPropertyHandlerConfiguration config(GlobalPropertyGetterCallback, - GlobalPropertySetterCallback, - GlobalPropertyQueryCallback, - GlobalPropertyDeleterCallback, - GlobalPropertyEnumeratorCallback, - CreateDataWrapper(env)); - object_template->SetHandler(config); + env->AssignToContext(ctx); - Local ctx = Context::New(env->isolate(), nullptr, object_template); + return scope.Escape(ctx); +} - if (ctx.IsEmpty()) { - env->ThrowError("Could not instantiate context"); - return Local(); - } - ctx->SetSecurityToken(env->context()->GetSecurityToken()); +// static +void ContextifyContext::Init(Environment* env, Local target) { + Local function_template = + FunctionTemplate::New(env->isolate()); + function_template->InstanceTemplate()->SetInternalFieldCount(1); + env->set_script_data_constructor_function(function_template->GetFunction()); - // We need to tie the lifetime of the sandbox object with the lifetime of - // newly created context. We do this by making them hold references to each - // other. The context can directly hold a reference to the sandbox as an - // embedder data field. However, we cannot hold a reference to a v8::Context - // directly in an Object, we instead hold onto the new context's global - // object instead (which then has a reference to the context). - ctx->SetEmbedderData(kSandboxObjectIndex, sandbox_obj); - sandbox_obj->SetPrivate(env->context(), - env->contextify_global_private_symbol(), - ctx->Global()); + env->SetMethod(target, "runInDebugContext", RunInDebugContext); + env->SetMethod(target, "makeContext", MakeContext); + env->SetMethod(target, "isContext", IsContext); +} - env->AssignToContext(ctx); - return scope.Escape(ctx); +// static +void ContextifyContext::RunInDebugContext(const FunctionCallbackInfo& args) { + Local script_source(args[0]->ToString(args.GetIsolate())); + if (script_source.IsEmpty()) + return; // Exception pending. + Local debug_context = Debug::GetDebugContext(args.GetIsolate()); + Environment* env = Environment::GetCurrent(args); + if (debug_context.IsEmpty()) { + // Force-load the debug context. + auto dummy_event_listener = [] (const Debug::EventDetails&) {}; + Debug::SetDebugEventListener(args.GetIsolate(), dummy_event_listener); + debug_context = Debug::GetDebugContext(args.GetIsolate()); + CHECK(!debug_context.IsEmpty()); + // Ensure that the debug context has an Environment assigned in case + // a fatal error is raised. The fatal exception handler in node.cc + // is not equipped to deal with contexts that don't have one and + // can't easily be taught that due to a deficiency in the V8 API: + // there is no way for the embedder to tell if the data index is + // in use. + const int index = Environment::kContextEmbedderDataIndex; + debug_context->SetAlignedPointerInEmbedderData(index, env); } + Context::Scope context_scope(debug_context); + MaybeLocal