OLD | NEW |
| (Empty) |
1 // Copyright 2016 the V8 project authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "src/inspector/V8Debugger.h" | |
6 | |
7 #include "src/inspector/DebuggerScript.h" | |
8 #include "src/inspector/ScriptBreakpoint.h" | |
9 #include "src/inspector/StringUtil.h" | |
10 #include "src/inspector/V8DebuggerAgentImpl.h" | |
11 #include "src/inspector/V8InspectorImpl.h" | |
12 #include "src/inspector/V8InternalValueType.h" | |
13 #include "src/inspector/V8StackTraceImpl.h" | |
14 #include "src/inspector/V8ValueCopier.h" | |
15 #include "src/inspector/protocol/Protocol.h" | |
16 | |
17 namespace v8_inspector { | |
18 | |
19 namespace { | |
20 const char stepIntoV8MethodName[] = "stepIntoStatement"; | |
21 const char stepOutV8MethodName[] = "stepOutOfFunction"; | |
22 static const char v8AsyncTaskEventEnqueue[] = "enqueue"; | |
23 static const char v8AsyncTaskEventWillHandle[] = "willHandle"; | |
24 static const char v8AsyncTaskEventDidHandle[] = "didHandle"; | |
25 | |
26 inline v8::Local<v8::Boolean> v8Boolean(bool value, v8::Isolate* isolate) { | |
27 return value ? v8::True(isolate) : v8::False(isolate); | |
28 } | |
29 | |
30 } // namespace | |
31 | |
32 static bool inLiveEditScope = false; | |
33 | |
34 v8::MaybeLocal<v8::Value> V8Debugger::callDebuggerMethod( | |
35 const char* functionName, int argc, v8::Local<v8::Value> argv[]) { | |
36 v8::MicrotasksScope microtasks(m_isolate, | |
37 v8::MicrotasksScope::kDoNotRunMicrotasks); | |
38 DCHECK(m_isolate->InContext()); | |
39 v8::Local<v8::Context> context = m_isolate->GetCurrentContext(); | |
40 v8::Local<v8::Object> debuggerScript = m_debuggerScript.Get(m_isolate); | |
41 v8::Local<v8::Function> function = v8::Local<v8::Function>::Cast( | |
42 debuggerScript | |
43 ->Get(context, toV8StringInternalized(m_isolate, functionName)) | |
44 .ToLocalChecked()); | |
45 return function->Call(context, debuggerScript, argc, argv); | |
46 } | |
47 | |
48 V8Debugger::V8Debugger(v8::Isolate* isolate, V8InspectorImpl* inspector) | |
49 : m_isolate(isolate), | |
50 m_inspector(inspector), | |
51 m_lastContextId(0), | |
52 m_enableCount(0), | |
53 m_breakpointsActivated(true), | |
54 m_runningNestedMessageLoop(false), | |
55 m_ignoreScriptParsedEventsCounter(0), | |
56 m_maxAsyncCallStackDepth(0) {} | |
57 | |
58 V8Debugger::~V8Debugger() {} | |
59 | |
60 void V8Debugger::enable() { | |
61 if (m_enableCount++) return; | |
62 DCHECK(!enabled()); | |
63 v8::HandleScope scope(m_isolate); | |
64 v8::Debug::SetDebugEventListener(m_isolate, &V8Debugger::v8DebugEventCallback, | |
65 v8::External::New(m_isolate, this)); | |
66 m_debuggerContext.Reset(m_isolate, v8::Debug::GetDebugContext(m_isolate)); | |
67 compileDebuggerScript(); | |
68 } | |
69 | |
70 void V8Debugger::disable() { | |
71 if (--m_enableCount) return; | |
72 DCHECK(enabled()); | |
73 clearBreakpoints(); | |
74 m_debuggerScript.Reset(); | |
75 m_debuggerContext.Reset(); | |
76 allAsyncTasksCanceled(); | |
77 v8::Debug::SetDebugEventListener(m_isolate, nullptr); | |
78 } | |
79 | |
80 bool V8Debugger::enabled() const { return !m_debuggerScript.IsEmpty(); } | |
81 | |
82 // static | |
83 int V8Debugger::contextId(v8::Local<v8::Context> context) { | |
84 v8::Local<v8::Value> data = | |
85 context->GetEmbedderData(static_cast<int>(v8::Context::kDebugIdIndex)); | |
86 if (data.IsEmpty() || !data->IsString()) return 0; | |
87 String16 dataString = toProtocolString(data.As<v8::String>()); | |
88 if (dataString.isEmpty()) return 0; | |
89 size_t commaPos = dataString.find(","); | |
90 if (commaPos == String16::kNotFound) return 0; | |
91 size_t commaPos2 = dataString.find(",", commaPos + 1); | |
92 if (commaPos2 == String16::kNotFound) return 0; | |
93 return dataString.substring(commaPos + 1, commaPos2 - commaPos - 1) | |
94 .toInteger(); | |
95 } | |
96 | |
97 // static | |
98 int V8Debugger::getGroupId(v8::Local<v8::Context> context) { | |
99 v8::Local<v8::Value> data = | |
100 context->GetEmbedderData(static_cast<int>(v8::Context::kDebugIdIndex)); | |
101 if (data.IsEmpty() || !data->IsString()) return 0; | |
102 String16 dataString = toProtocolString(data.As<v8::String>()); | |
103 if (dataString.isEmpty()) return 0; | |
104 size_t commaPos = dataString.find(","); | |
105 if (commaPos == String16::kNotFound) return 0; | |
106 return dataString.substring(0, commaPos).toInteger(); | |
107 } | |
108 | |
109 void V8Debugger::getCompiledScripts( | |
110 int contextGroupId, | |
111 std::vector<std::unique_ptr<V8DebuggerScript>>& result) { | |
112 v8::HandleScope scope(m_isolate); | |
113 v8::MicrotasksScope microtasks(m_isolate, | |
114 v8::MicrotasksScope::kDoNotRunMicrotasks); | |
115 v8::Local<v8::Context> context = debuggerContext(); | |
116 v8::Local<v8::Object> debuggerScript = m_debuggerScript.Get(m_isolate); | |
117 DCHECK(!debuggerScript->IsUndefined()); | |
118 v8::Local<v8::Function> getScriptsFunction = v8::Local<v8::Function>::Cast( | |
119 debuggerScript | |
120 ->Get(context, toV8StringInternalized(m_isolate, "getScripts")) | |
121 .ToLocalChecked()); | |
122 v8::Local<v8::Value> argv[] = {v8::Integer::New(m_isolate, contextGroupId)}; | |
123 v8::Local<v8::Value> value; | |
124 if (!getScriptsFunction->Call(context, debuggerScript, arraysize(argv), argv) | |
125 .ToLocal(&value)) | |
126 return; | |
127 DCHECK(value->IsArray()); | |
128 v8::Local<v8::Array> scriptsArray = v8::Local<v8::Array>::Cast(value); | |
129 result.reserve(scriptsArray->Length()); | |
130 for (unsigned i = 0; i < scriptsArray->Length(); ++i) { | |
131 v8::Local<v8::Object> scriptObject = v8::Local<v8::Object>::Cast( | |
132 scriptsArray->Get(context, v8::Integer::New(m_isolate, i)) | |
133 .ToLocalChecked()); | |
134 result.push_back(wrapUnique( | |
135 new V8DebuggerScript(context, scriptObject, inLiveEditScope))); | |
136 } | |
137 } | |
138 | |
139 String16 V8Debugger::setBreakpoint(const String16& sourceID, | |
140 const ScriptBreakpoint& scriptBreakpoint, | |
141 int* actualLineNumber, | |
142 int* actualColumnNumber) { | |
143 v8::HandleScope scope(m_isolate); | |
144 v8::Local<v8::Context> context = debuggerContext(); | |
145 v8::Context::Scope contextScope(context); | |
146 | |
147 v8::Local<v8::Object> info = v8::Object::New(m_isolate); | |
148 bool success = false; | |
149 success = info->Set(context, toV8StringInternalized(m_isolate, "sourceID"), | |
150 toV8String(m_isolate, sourceID)) | |
151 .FromMaybe(false); | |
152 DCHECK(success); | |
153 success = info->Set(context, toV8StringInternalized(m_isolate, "lineNumber"), | |
154 v8::Integer::New(m_isolate, scriptBreakpoint.lineNumber)) | |
155 .FromMaybe(false); | |
156 DCHECK(success); | |
157 success = | |
158 info->Set(context, toV8StringInternalized(m_isolate, "columnNumber"), | |
159 v8::Integer::New(m_isolate, scriptBreakpoint.columnNumber)) | |
160 .FromMaybe(false); | |
161 DCHECK(success); | |
162 success = info->Set(context, toV8StringInternalized(m_isolate, "condition"), | |
163 toV8String(m_isolate, scriptBreakpoint.condition)) | |
164 .FromMaybe(false); | |
165 DCHECK(success); | |
166 | |
167 v8::Local<v8::Function> setBreakpointFunction = v8::Local<v8::Function>::Cast( | |
168 m_debuggerScript.Get(m_isolate) | |
169 ->Get(context, toV8StringInternalized(m_isolate, "setBreakpoint")) | |
170 .ToLocalChecked()); | |
171 v8::Local<v8::Value> breakpointId = | |
172 v8::Debug::Call(debuggerContext(), setBreakpointFunction, info) | |
173 .ToLocalChecked(); | |
174 if (!breakpointId->IsString()) return ""; | |
175 *actualLineNumber = | |
176 info->Get(context, toV8StringInternalized(m_isolate, "lineNumber")) | |
177 .ToLocalChecked() | |
178 ->Int32Value(context) | |
179 .FromJust(); | |
180 *actualColumnNumber = | |
181 info->Get(context, toV8StringInternalized(m_isolate, "columnNumber")) | |
182 .ToLocalChecked() | |
183 ->Int32Value(context) | |
184 .FromJust(); | |
185 return toProtocolString(breakpointId.As<v8::String>()); | |
186 } | |
187 | |
188 void V8Debugger::removeBreakpoint(const String16& breakpointId) { | |
189 v8::HandleScope scope(m_isolate); | |
190 v8::Local<v8::Context> context = debuggerContext(); | |
191 v8::Context::Scope contextScope(context); | |
192 | |
193 v8::Local<v8::Object> info = v8::Object::New(m_isolate); | |
194 bool success = false; | |
195 success = | |
196 info->Set(context, toV8StringInternalized(m_isolate, "breakpointId"), | |
197 toV8String(m_isolate, breakpointId)) | |
198 .FromMaybe(false); | |
199 DCHECK(success); | |
200 | |
201 v8::Local<v8::Function> removeBreakpointFunction = | |
202 v8::Local<v8::Function>::Cast( | |
203 m_debuggerScript.Get(m_isolate) | |
204 ->Get(context, | |
205 toV8StringInternalized(m_isolate, "removeBreakpoint")) | |
206 .ToLocalChecked()); | |
207 v8::Debug::Call(debuggerContext(), removeBreakpointFunction, info) | |
208 .ToLocalChecked(); | |
209 } | |
210 | |
211 void V8Debugger::clearBreakpoints() { | |
212 v8::HandleScope scope(m_isolate); | |
213 v8::Local<v8::Context> context = debuggerContext(); | |
214 v8::Context::Scope contextScope(context); | |
215 | |
216 v8::Local<v8::Function> clearBreakpoints = v8::Local<v8::Function>::Cast( | |
217 m_debuggerScript.Get(m_isolate) | |
218 ->Get(context, toV8StringInternalized(m_isolate, "clearBreakpoints")) | |
219 .ToLocalChecked()); | |
220 v8::Debug::Call(debuggerContext(), clearBreakpoints).ToLocalChecked(); | |
221 } | |
222 | |
223 void V8Debugger::setBreakpointsActivated(bool activated) { | |
224 if (!enabled()) { | |
225 UNREACHABLE(); | |
226 return; | |
227 } | |
228 v8::HandleScope scope(m_isolate); | |
229 v8::Local<v8::Context> context = debuggerContext(); | |
230 v8::Context::Scope contextScope(context); | |
231 | |
232 v8::Local<v8::Object> info = v8::Object::New(m_isolate); | |
233 bool success = false; | |
234 success = info->Set(context, toV8StringInternalized(m_isolate, "enabled"), | |
235 v8::Boolean::New(m_isolate, activated)) | |
236 .FromMaybe(false); | |
237 DCHECK(success); | |
238 v8::Local<v8::Function> setBreakpointsActivated = | |
239 v8::Local<v8::Function>::Cast( | |
240 m_debuggerScript.Get(m_isolate) | |
241 ->Get(context, toV8StringInternalized(m_isolate, | |
242 "setBreakpointsActivated")) | |
243 .ToLocalChecked()); | |
244 v8::Debug::Call(debuggerContext(), setBreakpointsActivated, info) | |
245 .ToLocalChecked(); | |
246 | |
247 m_breakpointsActivated = activated; | |
248 } | |
249 | |
250 V8Debugger::PauseOnExceptionsState V8Debugger::getPauseOnExceptionsState() { | |
251 DCHECK(enabled()); | |
252 v8::HandleScope scope(m_isolate); | |
253 v8::Local<v8::Context> context = debuggerContext(); | |
254 v8::Context::Scope contextScope(context); | |
255 | |
256 v8::Local<v8::Value> argv[] = {v8::Undefined(m_isolate)}; | |
257 v8::Local<v8::Value> result = | |
258 callDebuggerMethod("pauseOnExceptionsState", 0, argv).ToLocalChecked(); | |
259 return static_cast<V8Debugger::PauseOnExceptionsState>( | |
260 result->Int32Value(context).FromJust()); | |
261 } | |
262 | |
263 void V8Debugger::setPauseOnExceptionsState( | |
264 PauseOnExceptionsState pauseOnExceptionsState) { | |
265 DCHECK(enabled()); | |
266 v8::HandleScope scope(m_isolate); | |
267 v8::Context::Scope contextScope(debuggerContext()); | |
268 | |
269 v8::Local<v8::Value> argv[] = { | |
270 v8::Int32::New(m_isolate, pauseOnExceptionsState)}; | |
271 callDebuggerMethod("setPauseOnExceptionsState", 1, argv); | |
272 } | |
273 | |
274 void V8Debugger::setPauseOnNextStatement(bool pause) { | |
275 if (m_runningNestedMessageLoop) return; | |
276 if (pause) | |
277 v8::Debug::DebugBreak(m_isolate); | |
278 else | |
279 v8::Debug::CancelDebugBreak(m_isolate); | |
280 } | |
281 | |
282 bool V8Debugger::canBreakProgram() { | |
283 if (!m_breakpointsActivated) return false; | |
284 return m_isolate->InContext(); | |
285 } | |
286 | |
287 void V8Debugger::breakProgram() { | |
288 if (isPaused()) { | |
289 DCHECK(!m_runningNestedMessageLoop); | |
290 v8::Local<v8::Value> exception; | |
291 v8::Local<v8::Array> hitBreakpoints; | |
292 handleProgramBreak(m_pausedContext, m_executionState, exception, | |
293 hitBreakpoints); | |
294 return; | |
295 } | |
296 | |
297 if (!canBreakProgram()) return; | |
298 | |
299 v8::HandleScope scope(m_isolate); | |
300 v8::Local<v8::Function> breakFunction; | |
301 if (!v8::Function::New(m_isolate->GetCurrentContext(), | |
302 &V8Debugger::breakProgramCallback, | |
303 v8::External::New(m_isolate, this), 0, | |
304 v8::ConstructorBehavior::kThrow) | |
305 .ToLocal(&breakFunction)) | |
306 return; | |
307 v8::Debug::Call(debuggerContext(), breakFunction).ToLocalChecked(); | |
308 } | |
309 | |
310 void V8Debugger::continueProgram() { | |
311 if (isPaused()) m_inspector->client()->quitMessageLoopOnPause(); | |
312 m_pausedContext.Clear(); | |
313 m_executionState.Clear(); | |
314 } | |
315 | |
316 void V8Debugger::stepIntoStatement() { | |
317 DCHECK(isPaused()); | |
318 DCHECK(!m_executionState.IsEmpty()); | |
319 v8::HandleScope handleScope(m_isolate); | |
320 v8::Local<v8::Value> argv[] = {m_executionState}; | |
321 callDebuggerMethod(stepIntoV8MethodName, 1, argv); | |
322 continueProgram(); | |
323 } | |
324 | |
325 void V8Debugger::stepOverStatement() { | |
326 DCHECK(isPaused()); | |
327 DCHECK(!m_executionState.IsEmpty()); | |
328 v8::HandleScope handleScope(m_isolate); | |
329 v8::Local<v8::Value> argv[] = {m_executionState}; | |
330 callDebuggerMethod("stepOverStatement", 1, argv); | |
331 continueProgram(); | |
332 } | |
333 | |
334 void V8Debugger::stepOutOfFunction() { | |
335 DCHECK(isPaused()); | |
336 DCHECK(!m_executionState.IsEmpty()); | |
337 v8::HandleScope handleScope(m_isolate); | |
338 v8::Local<v8::Value> argv[] = {m_executionState}; | |
339 callDebuggerMethod(stepOutV8MethodName, 1, argv); | |
340 continueProgram(); | |
341 } | |
342 | |
343 void V8Debugger::clearStepping() { | |
344 DCHECK(enabled()); | |
345 v8::HandleScope scope(m_isolate); | |
346 v8::Context::Scope contextScope(debuggerContext()); | |
347 | |
348 v8::Local<v8::Value> argv[] = {v8::Undefined(m_isolate)}; | |
349 callDebuggerMethod("clearStepping", 0, argv); | |
350 } | |
351 | |
352 bool V8Debugger::setScriptSource( | |
353 const String16& sourceID, v8::Local<v8::String> newSource, bool dryRun, | |
354 ErrorString* error, | |
355 Maybe<protocol::Runtime::ExceptionDetails>* exceptionDetails, | |
356 JavaScriptCallFrames* newCallFrames, Maybe<bool>* stackChanged) { | |
357 class EnableLiveEditScope { | |
358 public: | |
359 explicit EnableLiveEditScope(v8::Isolate* isolate) : m_isolate(isolate) { | |
360 v8::Debug::SetLiveEditEnabled(m_isolate, true); | |
361 inLiveEditScope = true; | |
362 } | |
363 ~EnableLiveEditScope() { | |
364 v8::Debug::SetLiveEditEnabled(m_isolate, false); | |
365 inLiveEditScope = false; | |
366 } | |
367 | |
368 private: | |
369 v8::Isolate* m_isolate; | |
370 }; | |
371 | |
372 DCHECK(enabled()); | |
373 v8::HandleScope scope(m_isolate); | |
374 | |
375 std::unique_ptr<v8::Context::Scope> contextScope; | |
376 if (!isPaused()) | |
377 contextScope = wrapUnique(new v8::Context::Scope(debuggerContext())); | |
378 | |
379 v8::Local<v8::Value> argv[] = {toV8String(m_isolate, sourceID), newSource, | |
380 v8Boolean(dryRun, m_isolate)}; | |
381 | |
382 v8::Local<v8::Value> v8result; | |
383 { | |
384 EnableLiveEditScope enableLiveEditScope(m_isolate); | |
385 v8::TryCatch tryCatch(m_isolate); | |
386 tryCatch.SetVerbose(false); | |
387 v8::MaybeLocal<v8::Value> maybeResult = | |
388 callDebuggerMethod("liveEditScriptSource", 3, argv); | |
389 if (tryCatch.HasCaught()) { | |
390 v8::Local<v8::Message> message = tryCatch.Message(); | |
391 if (!message.IsEmpty()) | |
392 *error = toProtocolStringWithTypeCheck(message->Get()); | |
393 else | |
394 *error = "Unknown error."; | |
395 return false; | |
396 } | |
397 v8result = maybeResult.ToLocalChecked(); | |
398 } | |
399 DCHECK(!v8result.IsEmpty()); | |
400 v8::Local<v8::Context> context = m_isolate->GetCurrentContext(); | |
401 v8::Local<v8::Object> resultTuple = | |
402 v8result->ToObject(context).ToLocalChecked(); | |
403 int code = static_cast<int>(resultTuple->Get(context, 0) | |
404 .ToLocalChecked() | |
405 ->ToInteger(context) | |
406 .ToLocalChecked() | |
407 ->Value()); | |
408 switch (code) { | |
409 case 0: { | |
410 *stackChanged = resultTuple->Get(context, 1) | |
411 .ToLocalChecked() | |
412 ->BooleanValue(context) | |
413 .FromJust(); | |
414 // Call stack may have changed after if the edited function was on the | |
415 // stack. | |
416 if (!dryRun && isPaused()) { | |
417 JavaScriptCallFrames frames = currentCallFrames(); | |
418 newCallFrames->swap(frames); | |
419 } | |
420 return true; | |
421 } | |
422 // Compile error. | |
423 case 1: { | |
424 *exceptionDetails = | |
425 protocol::Runtime::ExceptionDetails::create() | |
426 .setExceptionId(m_inspector->nextExceptionId()) | |
427 .setText(toProtocolStringWithTypeCheck( | |
428 resultTuple->Get(context, 2).ToLocalChecked())) | |
429 .setLineNumber(resultTuple->Get(context, 3) | |
430 .ToLocalChecked() | |
431 ->ToInteger(context) | |
432 .ToLocalChecked() | |
433 ->Value() - | |
434 1) | |
435 .setColumnNumber(resultTuple->Get(context, 4) | |
436 .ToLocalChecked() | |
437 ->ToInteger(context) | |
438 .ToLocalChecked() | |
439 ->Value() - | |
440 1) | |
441 .build(); | |
442 return false; | |
443 } | |
444 } | |
445 *error = "Unknown error."; | |
446 return false; | |
447 } | |
448 | |
449 JavaScriptCallFrames V8Debugger::currentCallFrames(int limit) { | |
450 if (!m_isolate->InContext()) return JavaScriptCallFrames(); | |
451 v8::Local<v8::Value> currentCallFramesV8; | |
452 if (m_executionState.IsEmpty()) { | |
453 v8::Local<v8::Function> currentCallFramesFunction = | |
454 v8::Local<v8::Function>::Cast( | |
455 m_debuggerScript.Get(m_isolate) | |
456 ->Get(debuggerContext(), | |
457 toV8StringInternalized(m_isolate, "currentCallFrames")) | |
458 .ToLocalChecked()); | |
459 currentCallFramesV8 = | |
460 v8::Debug::Call(debuggerContext(), currentCallFramesFunction, | |
461 v8::Integer::New(m_isolate, limit)) | |
462 .ToLocalChecked(); | |
463 } else { | |
464 v8::Local<v8::Value> argv[] = {m_executionState, | |
465 v8::Integer::New(m_isolate, limit)}; | |
466 currentCallFramesV8 = | |
467 callDebuggerMethod("currentCallFrames", arraysize(argv), argv) | |
468 .ToLocalChecked(); | |
469 } | |
470 DCHECK(!currentCallFramesV8.IsEmpty()); | |
471 if (!currentCallFramesV8->IsArray()) return JavaScriptCallFrames(); | |
472 v8::Local<v8::Array> callFramesArray = currentCallFramesV8.As<v8::Array>(); | |
473 JavaScriptCallFrames callFrames; | |
474 for (size_t i = 0; i < callFramesArray->Length(); ++i) { | |
475 v8::Local<v8::Value> callFrameValue; | |
476 if (!callFramesArray->Get(debuggerContext(), i).ToLocal(&callFrameValue)) | |
477 return JavaScriptCallFrames(); | |
478 if (!callFrameValue->IsObject()) return JavaScriptCallFrames(); | |
479 v8::Local<v8::Object> callFrameObject = callFrameValue.As<v8::Object>(); | |
480 callFrames.push_back(JavaScriptCallFrame::create( | |
481 debuggerContext(), v8::Local<v8::Object>::Cast(callFrameObject))); | |
482 } | |
483 return callFrames; | |
484 } | |
485 | |
486 static V8Debugger* toV8Debugger(v8::Local<v8::Value> data) { | |
487 void* p = v8::Local<v8::External>::Cast(data)->Value(); | |
488 return static_cast<V8Debugger*>(p); | |
489 } | |
490 | |
491 void V8Debugger::breakProgramCallback( | |
492 const v8::FunctionCallbackInfo<v8::Value>& info) { | |
493 DCHECK_EQ(info.Length(), 2); | |
494 V8Debugger* thisPtr = toV8Debugger(info.Data()); | |
495 if (!thisPtr->enabled()) return; | |
496 v8::Local<v8::Context> pausedContext = | |
497 thisPtr->m_isolate->GetCurrentContext(); | |
498 v8::Local<v8::Value> exception; | |
499 v8::Local<v8::Array> hitBreakpoints; | |
500 thisPtr->handleProgramBreak(pausedContext, | |
501 v8::Local<v8::Object>::Cast(info[0]), exception, | |
502 hitBreakpoints); | |
503 } | |
504 | |
505 void V8Debugger::handleProgramBreak(v8::Local<v8::Context> pausedContext, | |
506 v8::Local<v8::Object> executionState, | |
507 v8::Local<v8::Value> exception, | |
508 v8::Local<v8::Array> hitBreakpointNumbers, | |
509 bool isPromiseRejection) { | |
510 // Don't allow nested breaks. | |
511 if (m_runningNestedMessageLoop) return; | |
512 | |
513 V8DebuggerAgentImpl* agent = | |
514 m_inspector->enabledDebuggerAgentForGroup(getGroupId(pausedContext)); | |
515 if (!agent) return; | |
516 | |
517 std::vector<String16> breakpointIds; | |
518 if (!hitBreakpointNumbers.IsEmpty()) { | |
519 breakpointIds.reserve(hitBreakpointNumbers->Length()); | |
520 for (size_t i = 0; i < hitBreakpointNumbers->Length(); i++) { | |
521 v8::Local<v8::Value> hitBreakpointNumber = | |
522 hitBreakpointNumbers->Get(debuggerContext(), i).ToLocalChecked(); | |
523 DCHECK(hitBreakpointNumber->IsInt32()); | |
524 breakpointIds.push_back(String16::fromInteger( | |
525 hitBreakpointNumber->Int32Value(debuggerContext()).FromJust())); | |
526 } | |
527 } | |
528 | |
529 m_pausedContext = pausedContext; | |
530 m_executionState = executionState; | |
531 V8DebuggerAgentImpl::SkipPauseRequest result = agent->didPause( | |
532 pausedContext, exception, breakpointIds, isPromiseRejection); | |
533 if (result == V8DebuggerAgentImpl::RequestNoSkip) { | |
534 m_runningNestedMessageLoop = true; | |
535 int groupId = getGroupId(pausedContext); | |
536 DCHECK(groupId); | |
537 m_inspector->client()->runMessageLoopOnPause(groupId); | |
538 // The agent may have been removed in the nested loop. | |
539 agent = | |
540 m_inspector->enabledDebuggerAgentForGroup(getGroupId(pausedContext)); | |
541 if (agent) agent->didContinue(); | |
542 m_runningNestedMessageLoop = false; | |
543 } | |
544 m_pausedContext.Clear(); | |
545 m_executionState.Clear(); | |
546 | |
547 if (result == V8DebuggerAgentImpl::RequestStepFrame) { | |
548 v8::Local<v8::Value> argv[] = {executionState}; | |
549 callDebuggerMethod("stepFrameStatement", 1, argv); | |
550 } else if (result == V8DebuggerAgentImpl::RequestStepInto) { | |
551 v8::Local<v8::Value> argv[] = {executionState}; | |
552 callDebuggerMethod(stepIntoV8MethodName, 1, argv); | |
553 } else if (result == V8DebuggerAgentImpl::RequestStepOut) { | |
554 v8::Local<v8::Value> argv[] = {executionState}; | |
555 callDebuggerMethod(stepOutV8MethodName, 1, argv); | |
556 } | |
557 } | |
558 | |
559 void V8Debugger::v8DebugEventCallback( | |
560 const v8::Debug::EventDetails& eventDetails) { | |
561 V8Debugger* thisPtr = toV8Debugger(eventDetails.GetCallbackData()); | |
562 thisPtr->handleV8DebugEvent(eventDetails); | |
563 } | |
564 | |
565 v8::Local<v8::Value> V8Debugger::callInternalGetterFunction( | |
566 v8::Local<v8::Object> object, const char* functionName) { | |
567 v8::MicrotasksScope microtasks(m_isolate, | |
568 v8::MicrotasksScope::kDoNotRunMicrotasks); | |
569 v8::Local<v8::Value> getterValue = | |
570 object | |
571 ->Get(m_isolate->GetCurrentContext(), | |
572 toV8StringInternalized(m_isolate, functionName)) | |
573 .ToLocalChecked(); | |
574 DCHECK(!getterValue.IsEmpty() && getterValue->IsFunction()); | |
575 return v8::Local<v8::Function>::Cast(getterValue) | |
576 ->Call(m_isolate->GetCurrentContext(), object, 0, 0) | |
577 .ToLocalChecked(); | |
578 } | |
579 | |
580 void V8Debugger::handleV8DebugEvent( | |
581 const v8::Debug::EventDetails& eventDetails) { | |
582 if (!enabled()) return; | |
583 v8::DebugEvent event = eventDetails.GetEvent(); | |
584 if (event != v8::AsyncTaskEvent && event != v8::Break && | |
585 event != v8::Exception && event != v8::AfterCompile && | |
586 event != v8::BeforeCompile && event != v8::CompileError) | |
587 return; | |
588 | |
589 v8::Local<v8::Context> eventContext = eventDetails.GetEventContext(); | |
590 DCHECK(!eventContext.IsEmpty()); | |
591 | |
592 if (event == v8::AsyncTaskEvent) { | |
593 v8::HandleScope scope(m_isolate); | |
594 handleV8AsyncTaskEvent(eventContext, eventDetails.GetExecutionState(), | |
595 eventDetails.GetEventData()); | |
596 return; | |
597 } | |
598 | |
599 V8DebuggerAgentImpl* agent = | |
600 m_inspector->enabledDebuggerAgentForGroup(getGroupId(eventContext)); | |
601 if (agent) { | |
602 v8::HandleScope scope(m_isolate); | |
603 if (m_ignoreScriptParsedEventsCounter == 0 && | |
604 (event == v8::AfterCompile || event == v8::CompileError)) { | |
605 v8::Context::Scope contextScope(debuggerContext()); | |
606 v8::Local<v8::Value> argv[] = {eventDetails.GetEventData()}; | |
607 v8::Local<v8::Value> value = | |
608 callDebuggerMethod("getAfterCompileScript", 1, argv).ToLocalChecked(); | |
609 if (value->IsNull()) return; | |
610 DCHECK(value->IsObject()); | |
611 v8::Local<v8::Object> scriptObject = v8::Local<v8::Object>::Cast(value); | |
612 agent->didParseSource( | |
613 wrapUnique(new V8DebuggerScript(debuggerContext(), scriptObject, | |
614 inLiveEditScope)), | |
615 event == v8::AfterCompile); | |
616 } else if (event == v8::Exception) { | |
617 v8::Local<v8::Object> eventData = eventDetails.GetEventData(); | |
618 v8::Local<v8::Value> exception = | |
619 callInternalGetterFunction(eventData, "exception"); | |
620 v8::Local<v8::Value> promise = | |
621 callInternalGetterFunction(eventData, "promise"); | |
622 bool isPromiseRejection = !promise.IsEmpty() && promise->IsObject(); | |
623 handleProgramBreak(eventContext, eventDetails.GetExecutionState(), | |
624 exception, v8::Local<v8::Array>(), isPromiseRejection); | |
625 } else if (event == v8::Break) { | |
626 v8::Local<v8::Value> argv[] = {eventDetails.GetEventData()}; | |
627 v8::Local<v8::Value> hitBreakpoints = | |
628 callDebuggerMethod("getBreakpointNumbers", 1, argv).ToLocalChecked(); | |
629 DCHECK(hitBreakpoints->IsArray()); | |
630 handleProgramBreak(eventContext, eventDetails.GetExecutionState(), | |
631 v8::Local<v8::Value>(), | |
632 hitBreakpoints.As<v8::Array>()); | |
633 } | |
634 } | |
635 } | |
636 | |
637 void V8Debugger::handleV8AsyncTaskEvent(v8::Local<v8::Context> context, | |
638 v8::Local<v8::Object> executionState, | |
639 v8::Local<v8::Object> eventData) { | |
640 if (!m_maxAsyncCallStackDepth) return; | |
641 | |
642 String16 type = toProtocolStringWithTypeCheck( | |
643 callInternalGetterFunction(eventData, "type")); | |
644 String16 name = toProtocolStringWithTypeCheck( | |
645 callInternalGetterFunction(eventData, "name")); | |
646 int id = callInternalGetterFunction(eventData, "id") | |
647 ->ToInteger(context) | |
648 .ToLocalChecked() | |
649 ->Value(); | |
650 // Async task events from Promises are given misaligned pointers to prevent | |
651 // from overlapping with other Blink task identifiers. There is a single | |
652 // namespace of such ids, managed by src/js/promise.js. | |
653 void* ptr = reinterpret_cast<void*>(id * 2 + 1); | |
654 if (type == v8AsyncTaskEventEnqueue) | |
655 asyncTaskScheduled(name, ptr, false); | |
656 else if (type == v8AsyncTaskEventWillHandle) | |
657 asyncTaskStarted(ptr); | |
658 else if (type == v8AsyncTaskEventDidHandle) | |
659 asyncTaskFinished(ptr); | |
660 else | |
661 UNREACHABLE(); | |
662 } | |
663 | |
664 V8StackTraceImpl* V8Debugger::currentAsyncCallChain() { | |
665 if (!m_currentStacks.size()) return nullptr; | |
666 return m_currentStacks.back().get(); | |
667 } | |
668 | |
669 void V8Debugger::compileDebuggerScript() { | |
670 if (!m_debuggerScript.IsEmpty()) { | |
671 UNREACHABLE(); | |
672 return; | |
673 } | |
674 | |
675 v8::HandleScope scope(m_isolate); | |
676 v8::Context::Scope contextScope(debuggerContext()); | |
677 | |
678 v8::Local<v8::String> scriptValue = | |
679 v8::String::NewFromUtf8(m_isolate, DebuggerScript_js, | |
680 v8::NewStringType::kInternalized, | |
681 sizeof(DebuggerScript_js)) | |
682 .ToLocalChecked(); | |
683 v8::Local<v8::Value> value; | |
684 if (!m_inspector->compileAndRunInternalScript(debuggerContext(), scriptValue) | |
685 .ToLocal(&value)) { | |
686 UNREACHABLE(); | |
687 return; | |
688 } | |
689 DCHECK(value->IsObject()); | |
690 m_debuggerScript.Reset(m_isolate, value.As<v8::Object>()); | |
691 } | |
692 | |
693 v8::Local<v8::Context> V8Debugger::debuggerContext() const { | |
694 DCHECK(!m_debuggerContext.IsEmpty()); | |
695 return m_debuggerContext.Get(m_isolate); | |
696 } | |
697 | |
698 v8::MaybeLocal<v8::Value> V8Debugger::functionScopes( | |
699 v8::Local<v8::Context> context, v8::Local<v8::Function> function) { | |
700 if (!enabled()) { | |
701 UNREACHABLE(); | |
702 return v8::Local<v8::Value>::New(m_isolate, v8::Undefined(m_isolate)); | |
703 } | |
704 v8::Local<v8::Value> argv[] = {function}; | |
705 v8::Local<v8::Value> scopesValue; | |
706 if (!callDebuggerMethod("getFunctionScopes", 1, argv).ToLocal(&scopesValue)) | |
707 return v8::MaybeLocal<v8::Value>(); | |
708 v8::Local<v8::Value> copied; | |
709 if (!copyValueFromDebuggerContext(m_isolate, debuggerContext(), context, | |
710 scopesValue) | |
711 .ToLocal(&copied) || | |
712 !copied->IsArray()) | |
713 return v8::MaybeLocal<v8::Value>(); | |
714 if (!markAsInternal(context, v8::Local<v8::Array>::Cast(copied), | |
715 V8InternalValueType::kScopeList)) | |
716 return v8::MaybeLocal<v8::Value>(); | |
717 if (!markArrayEntriesAsInternal(context, v8::Local<v8::Array>::Cast(copied), | |
718 V8InternalValueType::kScope)) | |
719 return v8::MaybeLocal<v8::Value>(); | |
720 return copied; | |
721 } | |
722 | |
723 v8::MaybeLocal<v8::Array> V8Debugger::internalProperties( | |
724 v8::Local<v8::Context> context, v8::Local<v8::Value> value) { | |
725 v8::Local<v8::Array> properties; | |
726 if (!v8::Debug::GetInternalProperties(m_isolate, value).ToLocal(&properties)) | |
727 return v8::MaybeLocal<v8::Array>(); | |
728 if (value->IsFunction()) { | |
729 v8::Local<v8::Function> function = value.As<v8::Function>(); | |
730 v8::Local<v8::Value> location = functionLocation(context, function); | |
731 if (location->IsObject()) { | |
732 createDataProperty( | |
733 context, properties, properties->Length(), | |
734 toV8StringInternalized(m_isolate, "[[FunctionLocation]]")); | |
735 createDataProperty(context, properties, properties->Length(), location); | |
736 } | |
737 if (function->IsGeneratorFunction()) { | |
738 createDataProperty(context, properties, properties->Length(), | |
739 toV8StringInternalized(m_isolate, "[[IsGenerator]]")); | |
740 createDataProperty(context, properties, properties->Length(), | |
741 v8::True(m_isolate)); | |
742 } | |
743 } | |
744 if (!enabled()) return properties; | |
745 if (value->IsMap() || value->IsWeakMap() || value->IsSet() || | |
746 value->IsWeakSet() || value->IsSetIterator() || value->IsMapIterator()) { | |
747 v8::Local<v8::Value> entries = | |
748 collectionEntries(context, v8::Local<v8::Object>::Cast(value)); | |
749 if (entries->IsArray()) { | |
750 createDataProperty(context, properties, properties->Length(), | |
751 toV8StringInternalized(m_isolate, "[[Entries]]")); | |
752 createDataProperty(context, properties, properties->Length(), entries); | |
753 } | |
754 } | |
755 if (value->IsGeneratorObject()) { | |
756 v8::Local<v8::Value> location = | |
757 generatorObjectLocation(context, v8::Local<v8::Object>::Cast(value)); | |
758 if (location->IsObject()) { | |
759 createDataProperty( | |
760 context, properties, properties->Length(), | |
761 toV8StringInternalized(m_isolate, "[[GeneratorLocation]]")); | |
762 createDataProperty(context, properties, properties->Length(), location); | |
763 } | |
764 } | |
765 if (value->IsFunction()) { | |
766 v8::Local<v8::Function> function = value.As<v8::Function>(); | |
767 v8::Local<v8::Value> boundFunction = function->GetBoundFunction(); | |
768 v8::Local<v8::Value> scopes; | |
769 if (boundFunction->IsUndefined() && | |
770 functionScopes(context, function).ToLocal(&scopes)) { | |
771 createDataProperty(context, properties, properties->Length(), | |
772 toV8StringInternalized(m_isolate, "[[Scopes]]")); | |
773 createDataProperty(context, properties, properties->Length(), scopes); | |
774 } | |
775 } | |
776 return properties; | |
777 } | |
778 | |
779 v8::Local<v8::Value> V8Debugger::collectionEntries( | |
780 v8::Local<v8::Context> context, v8::Local<v8::Object> object) { | |
781 if (!enabled()) { | |
782 UNREACHABLE(); | |
783 return v8::Undefined(m_isolate); | |
784 } | |
785 v8::Local<v8::Value> argv[] = {object}; | |
786 v8::Local<v8::Value> entriesValue = | |
787 callDebuggerMethod("getCollectionEntries", 1, argv).ToLocalChecked(); | |
788 v8::Local<v8::Value> copied; | |
789 if (!copyValueFromDebuggerContext(m_isolate, debuggerContext(), context, | |
790 entriesValue) | |
791 .ToLocal(&copied) || | |
792 !copied->IsArray()) | |
793 return v8::Undefined(m_isolate); | |
794 if (!markArrayEntriesAsInternal(context, v8::Local<v8::Array>::Cast(copied), | |
795 V8InternalValueType::kEntry)) | |
796 return v8::Undefined(m_isolate); | |
797 return copied; | |
798 } | |
799 | |
800 v8::Local<v8::Value> V8Debugger::generatorObjectLocation( | |
801 v8::Local<v8::Context> context, v8::Local<v8::Object> object) { | |
802 if (!enabled()) { | |
803 UNREACHABLE(); | |
804 return v8::Null(m_isolate); | |
805 } | |
806 v8::Local<v8::Value> argv[] = {object}; | |
807 v8::Local<v8::Value> location = | |
808 callDebuggerMethod("getGeneratorObjectLocation", 1, argv) | |
809 .ToLocalChecked(); | |
810 v8::Local<v8::Value> copied; | |
811 if (!copyValueFromDebuggerContext(m_isolate, debuggerContext(), context, | |
812 location) | |
813 .ToLocal(&copied) || | |
814 !copied->IsObject()) | |
815 return v8::Null(m_isolate); | |
816 if (!markAsInternal(context, v8::Local<v8::Object>::Cast(copied), | |
817 V8InternalValueType::kLocation)) | |
818 return v8::Null(m_isolate); | |
819 return copied; | |
820 } | |
821 | |
822 v8::Local<v8::Value> V8Debugger::functionLocation( | |
823 v8::Local<v8::Context> context, v8::Local<v8::Function> function) { | |
824 int scriptId = function->ScriptId(); | |
825 if (scriptId == v8::UnboundScript::kNoScriptId) return v8::Null(m_isolate); | |
826 int lineNumber = function->GetScriptLineNumber(); | |
827 int columnNumber = function->GetScriptColumnNumber(); | |
828 if (lineNumber == v8::Function::kLineOffsetNotFound || | |
829 columnNumber == v8::Function::kLineOffsetNotFound) | |
830 return v8::Null(m_isolate); | |
831 v8::Local<v8::Object> location = v8::Object::New(m_isolate); | |
832 if (!location->SetPrototype(context, v8::Null(m_isolate)).FromMaybe(false)) | |
833 return v8::Null(m_isolate); | |
834 if (!createDataProperty( | |
835 context, location, toV8StringInternalized(m_isolate, "scriptId"), | |
836 toV8String(m_isolate, String16::fromInteger(scriptId))) | |
837 .FromMaybe(false)) | |
838 return v8::Null(m_isolate); | |
839 if (!createDataProperty(context, location, | |
840 toV8StringInternalized(m_isolate, "lineNumber"), | |
841 v8::Integer::New(m_isolate, lineNumber)) | |
842 .FromMaybe(false)) | |
843 return v8::Null(m_isolate); | |
844 if (!createDataProperty(context, location, | |
845 toV8StringInternalized(m_isolate, "columnNumber"), | |
846 v8::Integer::New(m_isolate, columnNumber)) | |
847 .FromMaybe(false)) | |
848 return v8::Null(m_isolate); | |
849 if (!markAsInternal(context, location, V8InternalValueType::kLocation)) | |
850 return v8::Null(m_isolate); | |
851 return location; | |
852 } | |
853 | |
854 bool V8Debugger::isPaused() { return !m_pausedContext.IsEmpty(); } | |
855 | |
856 std::unique_ptr<V8StackTraceImpl> V8Debugger::createStackTrace( | |
857 v8::Local<v8::StackTrace> stackTrace) { | |
858 int contextGroupId = | |
859 m_isolate->InContext() ? getGroupId(m_isolate->GetCurrentContext()) : 0; | |
860 return V8StackTraceImpl::create(this, contextGroupId, stackTrace, | |
861 V8StackTraceImpl::maxCallStackSizeToCapture); | |
862 } | |
863 | |
864 int V8Debugger::markContext(const V8ContextInfo& info) { | |
865 DCHECK(info.context->GetIsolate() == m_isolate); | |
866 int contextId = ++m_lastContextId; | |
867 String16 debugData = String16::fromInteger(info.contextGroupId) + "," + | |
868 String16::fromInteger(contextId) + "," + | |
869 toString16(info.auxData); | |
870 v8::Context::Scope contextScope(info.context); | |
871 info.context->SetEmbedderData(static_cast<int>(v8::Context::kDebugIdIndex), | |
872 toV8String(m_isolate, debugData)); | |
873 return contextId; | |
874 } | |
875 | |
876 void V8Debugger::setAsyncCallStackDepth(V8DebuggerAgentImpl* agent, int depth) { | |
877 if (depth <= 0) | |
878 m_maxAsyncCallStackDepthMap.erase(agent); | |
879 else | |
880 m_maxAsyncCallStackDepthMap[agent] = depth; | |
881 | |
882 int maxAsyncCallStackDepth = 0; | |
883 for (const auto& pair : m_maxAsyncCallStackDepthMap) { | |
884 if (pair.second > maxAsyncCallStackDepth) | |
885 maxAsyncCallStackDepth = pair.second; | |
886 } | |
887 | |
888 if (m_maxAsyncCallStackDepth == maxAsyncCallStackDepth) return; | |
889 m_maxAsyncCallStackDepth = maxAsyncCallStackDepth; | |
890 if (!maxAsyncCallStackDepth) allAsyncTasksCanceled(); | |
891 } | |
892 | |
893 void V8Debugger::asyncTaskScheduled(const StringView& taskName, void* task, | |
894 bool recurring) { | |
895 if (!m_maxAsyncCallStackDepth) return; | |
896 asyncTaskScheduled(toString16(taskName), task, recurring); | |
897 } | |
898 | |
899 void V8Debugger::asyncTaskScheduled(const String16& taskName, void* task, | |
900 bool recurring) { | |
901 if (!m_maxAsyncCallStackDepth) return; | |
902 v8::HandleScope scope(m_isolate); | |
903 int contextGroupId = | |
904 m_isolate->InContext() ? getGroupId(m_isolate->GetCurrentContext()) : 0; | |
905 std::unique_ptr<V8StackTraceImpl> chain = V8StackTraceImpl::capture( | |
906 this, contextGroupId, V8StackTraceImpl::maxCallStackSizeToCapture, | |
907 taskName); | |
908 if (chain) { | |
909 m_asyncTaskStacks[task] = std::move(chain); | |
910 if (recurring) m_recurringTasks.insert(task); | |
911 } | |
912 } | |
913 | |
914 void V8Debugger::asyncTaskCanceled(void* task) { | |
915 if (!m_maxAsyncCallStackDepth) return; | |
916 m_asyncTaskStacks.erase(task); | |
917 m_recurringTasks.erase(task); | |
918 } | |
919 | |
920 void V8Debugger::asyncTaskStarted(void* task) { | |
921 if (!m_maxAsyncCallStackDepth) return; | |
922 m_currentTasks.push_back(task); | |
923 AsyncTaskToStackTrace::iterator stackIt = m_asyncTaskStacks.find(task); | |
924 // Needs to support following order of events: | |
925 // - asyncTaskScheduled | |
926 // <-- attached here --> | |
927 // - asyncTaskStarted | |
928 // - asyncTaskCanceled <-- canceled before finished | |
929 // <-- async stack requested here --> | |
930 // - asyncTaskFinished | |
931 std::unique_ptr<V8StackTraceImpl> stack; | |
932 if (stackIt != m_asyncTaskStacks.end() && stackIt->second) | |
933 stack = stackIt->second->cloneImpl(); | |
934 m_currentStacks.push_back(std::move(stack)); | |
935 } | |
936 | |
937 void V8Debugger::asyncTaskFinished(void* task) { | |
938 if (!m_maxAsyncCallStackDepth) return; | |
939 // We could start instrumenting half way and the stack is empty. | |
940 if (!m_currentStacks.size()) return; | |
941 | |
942 DCHECK(m_currentTasks.back() == task); | |
943 m_currentTasks.pop_back(); | |
944 | |
945 m_currentStacks.pop_back(); | |
946 if (m_recurringTasks.find(task) == m_recurringTasks.end()) | |
947 m_asyncTaskStacks.erase(task); | |
948 } | |
949 | |
950 void V8Debugger::allAsyncTasksCanceled() { | |
951 m_asyncTaskStacks.clear(); | |
952 m_recurringTasks.clear(); | |
953 m_currentStacks.clear(); | |
954 m_currentTasks.clear(); | |
955 } | |
956 | |
957 void V8Debugger::muteScriptParsedEvents() { | |
958 ++m_ignoreScriptParsedEventsCounter; | |
959 } | |
960 | |
961 void V8Debugger::unmuteScriptParsedEvents() { | |
962 --m_ignoreScriptParsedEventsCounter; | |
963 DCHECK_GE(m_ignoreScriptParsedEventsCounter, 0); | |
964 } | |
965 | |
966 std::unique_ptr<V8StackTraceImpl> V8Debugger::captureStackTrace( | |
967 bool fullStack) { | |
968 if (!m_isolate->InContext()) return nullptr; | |
969 | |
970 v8::HandleScope handles(m_isolate); | |
971 int contextGroupId = getGroupId(m_isolate->GetCurrentContext()); | |
972 if (!contextGroupId) return nullptr; | |
973 | |
974 size_t stackSize = | |
975 fullStack ? V8StackTraceImpl::maxCallStackSizeToCapture : 1; | |
976 if (m_inspector->enabledRuntimeAgentForGroup(contextGroupId)) | |
977 stackSize = V8StackTraceImpl::maxCallStackSizeToCapture; | |
978 | |
979 return V8StackTraceImpl::capture(this, contextGroupId, stackSize); | |
980 } | |
981 | |
982 } // namespace v8_inspector | |
OLD | NEW |