Skip to content

Commit

Permalink
[WIP] src, inspector: support opted-in VM contexts
Browse files Browse the repository at this point in the history
Based on work by Bradley Farias <[email protected]> and Eugene
Ostroukhov <[email protected]>.

TODO: V8's console is injected unconditionally, which may be not desirable.

Fixes: nodejs#7593
Refs: nodejs#9272
  • Loading branch information
TimothyGu committed Jul 14, 2017
1 parent ff88cf4 commit 9f12ee8
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 12 deletions.
45 changes: 43 additions & 2 deletions lib/inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

const EventEmitter = require('events');
const util = require('util');
const { connect, open, url } = process.binding('inspector');
const { isContext } = process.binding('contextify');
const {
connect,
open,
url,
attachContext: _attachContext,
detachContext: _detachContext
} = process.binding('inspector');

if (!connect)
throw new Error('Inspector is not available');
Expand Down Expand Up @@ -86,9 +93,43 @@ 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(sandbox, {
name = `vm Module Context ${ctxIdx}`,
origin
} = {}) {
checkSandbox(sandbox);
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(sandbox, name, origin);
}

function detachContext(sandbox) {
checkSandbox(sandbox);
_detachContext(sandbox);
}

module.exports = {
open: (port, host, wait) => open(port, host, !!wait),
close: process._debugEnd,
url: url,
Session
Session,
attachContext,
detachContext
};
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -625,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)',
Expand Down
120 changes: 110 additions & 10 deletions src/inspector_agent.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
#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"
#include "zlib.h"

#include "libplatform/libplatform.h"

#include <algorithm>
#include <string.h>
#include <vector>

Expand All @@ -21,6 +23,9 @@
namespace node {
namespace inspector {
namespace {

using node::contextify::ContextifyContext;

using v8::Context;
using v8::External;
using v8::Function;
Expand All @@ -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) {}
Expand Down Expand Up @@ -376,9 +385,61 @@ void CallAndPauseOnStart(
}
}

// Used in NodeInspectorClient::currentTimeMS() below.
const int NANOS_PER_MSEC = 1000000;
const int CONTEXT_GROUP_ID = 1;
void AttachContext(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsObject()) {
env->ThrowTypeError("sandbox must be an object");
return;
}
Local<Object> sandbox = args[0].As<Object>();
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}");
env->inspector_agent()->ContextCreated(info);
}

void DetachContext(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
if (!args[0]->IsObject()) {
env->ThrowTypeError("sandbox must be an object");
return;
}
Local<Object> sandbox = args[0].As<Object>();
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:
Expand Down Expand Up @@ -459,11 +520,24 @@ class NodeInspectorClient : public v8_inspector::V8InspectorClient {
return uv_hrtime() * 1.0 / NANOS_PER_MSEC;
}

void contextCreated(Local<Context> context, const std::string& name) {
std::unique_ptr<StringBuffer> 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<StringBuffer> name_buffer = Utf8ToStringView(info->name());
v8_inspector::V8ContextInfo v8_info(info->context(env_->isolate()),
info->group_id(),
name_buffer->string());

std::unique_ptr<StringBuffer> origin_buffer;
std::unique_ptr<StringBuffer> 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> context) {
Expand Down Expand Up @@ -546,14 +620,36 @@ Agent::Agent(Environment* env) : parent_env_(env),
Agent::~Agent() {
}

void Agent::ContextCreated(const node::inspector::ContextInfo* info) {
contexts_.push_back(info);
client_->contextCreated(info);
}

void Agent::ContextDestroyed(Local<Context> 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;
debug_options_ = options;
client_ =
std::unique_ptr<NodeInspectorClient>(
new NodeInspectorClient(parent_env_, platform));
client_->contextCreated(parent_env_->context(), "Node.js Main Context");
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,
Expand Down Expand Up @@ -627,7 +723,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();
}
Expand Down Expand Up @@ -713,6 +811,8 @@ void Agent::InitInspector(Local<Object> target, Local<Value> unused,
Environment* env = Environment::GetCurrent(context);
Agent* agent = env->inspector_agent();
env->SetMethod(target, "consoleCall", InspectorConsoleCall);
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);
Expand Down
35 changes: 35 additions & 0 deletions src/inspector_agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
#define SRC_INSPECTOR_AGENT_H_

#include <memory>
#include <vector>

#include <stddef.h>

#if !HAVE_INSPECTOR
#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.
Expand Down Expand Up @@ -46,6 +48,35 @@ class InspectorSessionDelegate {
class InspectorIo;
class NodeInspectorClient;

class ContextInfo {
public:
explicit ContextInfo(v8::Local<v8::Context> 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<v8::Context> 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<v8::Context> context_;
const int group_id_;
const char* name_;
const char* origin_;
const char* aux_data_;
};

class Agent {
public:
explicit Agent(node::Environment* env);
Expand All @@ -57,6 +88,9 @@ class Agent {
// Stop and destroy io_
void Stop();

void ContextCreated(const node::inspector::ContextInfo* info);
void ContextDestroyed(v8::Local<v8::Context> context);

bool IsStarted() { return !!client_; }

// IO thread started, and client connected
Expand Down Expand Up @@ -102,6 +136,7 @@ class Agent {
std::unique_ptr<NodeInspectorClient> client_;
std::unique_ptr<InspectorIo> io_;
v8::Platform* platform_;
std::vector<const node::inspector::ContextInfo*> contexts_;
bool enabled_;
std::string path_;
DebugOptions debug_options_;
Expand Down

0 comments on commit 9f12ee8

Please sign in to comment.