| // Copyright 2016 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/inspector/v8-debugger.h" |
| |
| #include "src/inspector/inspected-context.h" |
| #include "src/inspector/protocol/Protocol.h" |
| #include "src/inspector/string-util.h" |
| #include "src/inspector/v8-debugger-agent-impl.h" |
| #include "src/inspector/v8-inspector-impl.h" |
| #include "src/inspector/v8-inspector-session-impl.h" |
| #include "src/inspector/v8-internal-value-type.h" |
| #include "src/inspector/v8-runtime-agent-impl.h" |
| #include "src/inspector/v8-stack-trace-impl.h" |
| #include "src/inspector/v8-value-utils.h" |
| |
| #include "include/v8-util.h" |
| |
| namespace v8_inspector { |
| |
| namespace { |
| |
| static const int kMaxAsyncTaskStacks = 128 * 1024; |
| static const int kNoBreakpointId = 0; |
| |
| v8::MaybeLocal<v8::Array> collectionsEntries(v8::Local<v8::Context> context, |
| v8::Local<v8::Value> value) { |
| v8::Isolate* isolate = context->GetIsolate(); |
| v8::Local<v8::Array> entries; |
| bool isKeyValue = false; |
| if (!v8::debug::EntriesPreview(isolate, value, &isKeyValue).ToLocal(&entries)) |
| return v8::MaybeLocal<v8::Array>(); |
| |
| v8::Local<v8::Array> wrappedEntries = v8::Array::New(isolate); |
| CHECK(!isKeyValue || wrappedEntries->Length() % 2 == 0); |
| if (!wrappedEntries->SetPrototype(context, v8::Null(isolate)) |
| .FromMaybe(false)) |
| return v8::MaybeLocal<v8::Array>(); |
| for (uint32_t i = 0; i < entries->Length(); i += isKeyValue ? 2 : 1) { |
| v8::Local<v8::Value> item; |
| if (!entries->Get(context, i).ToLocal(&item)) continue; |
| v8::Local<v8::Value> value; |
| if (isKeyValue && !entries->Get(context, i + 1).ToLocal(&value)) continue; |
| v8::Local<v8::Object> wrapper = v8::Object::New(isolate); |
| if (!wrapper->SetPrototype(context, v8::Null(isolate)).FromMaybe(false)) |
| continue; |
| createDataProperty( |
| context, wrapper, |
| toV8StringInternalized(isolate, isKeyValue ? "key" : "value"), item); |
| if (isKeyValue) { |
| createDataProperty(context, wrapper, |
| toV8StringInternalized(isolate, "value"), value); |
| } |
| createDataProperty(context, wrappedEntries, wrappedEntries->Length(), |
| wrapper); |
| } |
| if (!markArrayEntriesAsInternal(context, wrappedEntries, |
| V8InternalValueType::kEntry)) { |
| return v8::MaybeLocal<v8::Array>(); |
| } |
| return wrappedEntries; |
| } |
| |
| v8::MaybeLocal<v8::Object> buildLocation(v8::Local<v8::Context> context, |
| int scriptId, int lineNumber, |
| int columnNumber) { |
| if (scriptId == v8::UnboundScript::kNoScriptId) |
| return v8::MaybeLocal<v8::Object>(); |
| if (lineNumber == v8::Function::kLineOffsetNotFound || |
| columnNumber == v8::Function::kLineOffsetNotFound) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| v8::Isolate* isolate = context->GetIsolate(); |
| v8::Local<v8::Object> location = v8::Object::New(isolate); |
| if (!location->SetPrototype(context, v8::Null(isolate)).FromMaybe(false)) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| if (!createDataProperty(context, location, |
| toV8StringInternalized(isolate, "scriptId"), |
| toV8String(isolate, String16::fromInteger(scriptId))) |
| .FromMaybe(false)) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| if (!createDataProperty(context, location, |
| toV8StringInternalized(isolate, "lineNumber"), |
| v8::Integer::New(isolate, lineNumber)) |
| .FromMaybe(false)) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| if (!createDataProperty(context, location, |
| toV8StringInternalized(isolate, "columnNumber"), |
| v8::Integer::New(isolate, columnNumber)) |
| .FromMaybe(false)) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| if (!markAsInternal(context, location, V8InternalValueType::kLocation)) { |
| return v8::MaybeLocal<v8::Object>(); |
| } |
| return location; |
| } |
| |
| v8::MaybeLocal<v8::Object> generatorObjectLocation( |
| v8::Local<v8::Context> context, v8::Local<v8::Value> value) { |
| if (!value->IsGeneratorObject()) return v8::MaybeLocal<v8::Object>(); |
| v8::Local<v8::debug::GeneratorObject> generatorObject = |
| v8::debug::GeneratorObject::Cast(value); |
| if (!generatorObject->IsSuspended()) { |
| v8::Local<v8::Function> func = generatorObject->Function(); |
| return buildLocation(context, func->ScriptId(), func->GetScriptLineNumber(), |
| func->GetScriptColumnNumber()); |
| } |
| v8::Local<v8::debug::Script> script; |
| if (!generatorObject->Script().ToLocal(&script)) |
| return v8::MaybeLocal<v8::Object>(); |
| v8::debug::Location suspendedLocation = generatorObject->SuspendedLocation(); |
| return buildLocation(context, script->Id(), suspendedLocation.GetLineNumber(), |
| suspendedLocation.GetColumnNumber()); |
| } |
| |
| template <typename Map> |
| void cleanupExpiredWeakPointers(Map& map) { |
| for (auto it = map.begin(); it != map.end();) { |
| if (it->second.expired()) { |
| it = map.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| } |
| |
| class MatchPrototypePredicate : public v8::debug::QueryObjectPredicate { |
| public: |
| MatchPrototypePredicate(V8InspectorImpl* inspector, |
| v8::Local<v8::Context> context, |
| v8::Local<v8::Object> prototype) |
| : m_inspector(inspector), m_context(context), m_prototype(prototype) {} |
| |
| bool Filter(v8::Local<v8::Object> object) override { |
| v8::Local<v8::Context> objectContext = object->CreationContext(); |
| if (objectContext != m_context) return false; |
| if (!m_inspector->client()->isInspectableHeapObject(object)) return false; |
| // Get prototype chain for current object until first visited prototype. |
| for (v8::Local<v8::Value> prototype = object->GetPrototype(); |
| prototype->IsObject(); |
| prototype = prototype.As<v8::Object>()->GetPrototype()) { |
| if (m_prototype == prototype) return true; |
| } |
| return false; |
| } |
| |
| private: |
| V8InspectorImpl* m_inspector; |
| v8::Local<v8::Context> m_context; |
| v8::Local<v8::Value> m_prototype; |
| }; |
| |
| } // namespace |
| |
| V8Debugger::V8Debugger(v8::Isolate* isolate, V8InspectorImpl* inspector) |
| : m_isolate(isolate), |
| m_inspector(inspector), |
| m_enableCount(0), |
| m_ignoreScriptParsedEventsCounter(0), |
| m_continueToLocationBreakpointId(kNoBreakpointId), |
| m_maxAsyncCallStacks(kMaxAsyncTaskStacks), |
| m_maxAsyncCallStackDepth(0), |
| m_pauseOnExceptionsState(v8::debug::NoBreakOnException), |
| m_wasmTranslation(isolate) {} |
| |
| V8Debugger::~V8Debugger() {} |
| |
| void V8Debugger::enable() { |
| if (m_enableCount++) return; |
| v8::HandleScope scope(m_isolate); |
| v8::debug::SetDebugDelegate(m_isolate, this); |
| v8::debug::SetOutOfMemoryCallback(m_isolate, &V8Debugger::v8OOMCallback, |
| this); |
| v8::debug::ChangeBreakOnException(m_isolate, v8::debug::NoBreakOnException); |
| m_pauseOnExceptionsState = v8::debug::NoBreakOnException; |
| } |
| |
| void V8Debugger::disable() { |
| if (--m_enableCount) return; |
| clearContinueToLocation(); |
| allAsyncTasksCanceled(); |
| m_taskWithScheduledBreak = nullptr; |
| m_taskWithScheduledBreakDebuggerId = String16(); |
| m_pauseOnAsyncCall = false; |
| m_wasmTranslation.Clear(); |
| v8::debug::SetDebugDelegate(m_isolate, nullptr); |
| v8::debug::SetOutOfMemoryCallback(m_isolate, nullptr, nullptr); |
| m_isolate->RestoreOriginalHeapLimit(); |
| } |
| |
| bool V8Debugger::isPausedInContextGroup(int contextGroupId) const { |
| return isPaused() && m_pausedContextGroupId == contextGroupId; |
| } |
| |
| bool V8Debugger::enabled() const { return m_enableCount > 0; } |
| |
| void V8Debugger::getCompiledScripts( |
| int contextGroupId, |
| std::vector<std::unique_ptr<V8DebuggerScript>>& result) { |
| v8::HandleScope scope(m_isolate); |
| v8::PersistentValueVector<v8::debug::Script> scripts(m_isolate); |
| v8::debug::GetLoadedScripts(m_isolate, scripts); |
| for (size_t i = 0; i < scripts.Size(); ++i) { |
| v8::Local<v8::debug::Script> script = scripts.Get(i); |
| if (!script->WasCompiled()) continue; |
| if (script->IsEmbedded()) { |
| result.push_back(V8DebuggerScript::Create(m_isolate, script, false)); |
| continue; |
| } |
| int contextId; |
| if (!script->ContextId().To(&contextId)) continue; |
| if (m_inspector->contextGroupId(contextId) != contextGroupId) continue; |
| result.push_back(V8DebuggerScript::Create(m_isolate, script, false)); |
| } |
| } |
| |
| void V8Debugger::setBreakpointsActive(bool active) { |
| if (!enabled()) { |
| UNREACHABLE(); |
| return; |
| } |
| m_breakpointsActiveCount += active ? 1 : -1; |
| v8::debug::SetBreakPointsActive(m_isolate, m_breakpointsActiveCount); |
| } |
| |
| v8::debug::ExceptionBreakState V8Debugger::getPauseOnExceptionsState() { |
| DCHECK(enabled()); |
| return m_pauseOnExceptionsState; |
| } |
| |
| void V8Debugger::setPauseOnExceptionsState( |
| v8::debug::ExceptionBreakState pauseOnExceptionsState) { |
| DCHECK(enabled()); |
| if (m_pauseOnExceptionsState == pauseOnExceptionsState) return; |
| v8::debug::ChangeBreakOnException(m_isolate, pauseOnExceptionsState); |
| m_pauseOnExceptionsState = pauseOnExceptionsState; |
| } |
| |
| void V8Debugger::setPauseOnNextStatement(bool pause, int targetContextGroupId) { |
| if (isPaused()) return; |
| DCHECK(targetContextGroupId); |
| if (!pause && m_targetContextGroupId && |
| m_targetContextGroupId != targetContextGroupId) { |
| return; |
| } |
| m_targetContextGroupId = targetContextGroupId; |
| m_breakRequested = pause; |
| if (pause) |
| v8::debug::DebugBreak(m_isolate); |
| else |
| v8::debug::CancelDebugBreak(m_isolate); |
| } |
| |
| bool V8Debugger::canBreakProgram() { |
| return !v8::debug::AllFramesOnStackAreBlackboxed(m_isolate); |
| } |
| |
| void V8Debugger::breakProgram(int targetContextGroupId) { |
| DCHECK(canBreakProgram()); |
| // Don't allow nested breaks. |
| if (isPaused()) return; |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| v8::debug::BreakRightNow(m_isolate); |
| } |
| |
| void V8Debugger::continueProgram(int targetContextGroupId) { |
| if (m_pausedContextGroupId != targetContextGroupId) return; |
| if (isPaused()) m_inspector->client()->quitMessageLoopOnPause(); |
| } |
| |
| void V8Debugger::breakProgramOnAssert(int targetContextGroupId) { |
| if (!enabled()) return; |
| if (m_pauseOnExceptionsState == v8::debug::NoBreakOnException) return; |
| // Don't allow nested breaks. |
| if (isPaused()) return; |
| if (!canBreakProgram()) return; |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| m_scheduledAssertBreak = true; |
| v8::debug::BreakRightNow(m_isolate); |
| } |
| |
| void V8Debugger::stepIntoStatement(int targetContextGroupId, |
| bool breakOnAsyncCall) { |
| DCHECK(isPaused()); |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| m_pauseOnAsyncCall = breakOnAsyncCall; |
| v8::debug::PrepareStep(m_isolate, v8::debug::StepIn); |
| continueProgram(targetContextGroupId); |
| } |
| |
| void V8Debugger::stepOverStatement(int targetContextGroupId) { |
| DCHECK(isPaused()); |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| v8::debug::PrepareStep(m_isolate, v8::debug::StepNext); |
| continueProgram(targetContextGroupId); |
| } |
| |
| void V8Debugger::stepOutOfFunction(int targetContextGroupId) { |
| DCHECK(isPaused()); |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| v8::debug::PrepareStep(m_isolate, v8::debug::StepOut); |
| continueProgram(targetContextGroupId); |
| } |
| |
| void V8Debugger::scheduleStepIntoAsync( |
| std::unique_ptr<ScheduleStepIntoAsyncCallback> callback, |
| int targetContextGroupId) { |
| DCHECK(isPaused()); |
| DCHECK(targetContextGroupId); |
| if (m_stepIntoAsyncCallback) { |
| m_stepIntoAsyncCallback->sendFailure(Response::Error( |
| "Current scheduled step into async was overriden with new one.")); |
| } |
| m_targetContextGroupId = targetContextGroupId; |
| m_stepIntoAsyncCallback = std::move(callback); |
| } |
| |
| void V8Debugger::pauseOnAsyncCall(int targetContextGroupId, uintptr_t task, |
| const String16& debuggerId) { |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| |
| m_taskWithScheduledBreak = reinterpret_cast<void*>(task); |
| m_taskWithScheduledBreakDebuggerId = debuggerId; |
| } |
| |
| Response V8Debugger::continueToLocation( |
| int targetContextGroupId, V8DebuggerScript* script, |
| std::unique_ptr<protocol::Debugger::Location> location, |
| const String16& targetCallFrames) { |
| DCHECK(isPaused()); |
| DCHECK(targetContextGroupId); |
| m_targetContextGroupId = targetContextGroupId; |
| v8::debug::Location v8Location(location->getLineNumber(), |
| location->getColumnNumber(0)); |
| if (script->setBreakpoint(String16(), &v8Location, |
| &m_continueToLocationBreakpointId)) { |
| m_continueToLocationTargetCallFrames = targetCallFrames; |
| if (m_continueToLocationTargetCallFrames != |
| protocol::Debugger::ContinueToLocation::TargetCallFramesEnum::Any) { |
| m_continueToLocationStack = captureStackTrace(true); |
| DCHECK(m_continueToLocationStack); |
| } |
| continueProgram(targetContextGroupId); |
| // TODO(kozyatinskiy): Return actual line and column number. |
| return Response::OK(); |
| } else { |
| return Response::Error("Cannot continue to specified location"); |
| } |
| } |
| |
| bool V8Debugger::shouldContinueToCurrentLocation() { |
| if (m_continueToLocationTargetCallFrames == |
| protocol::Debugger::ContinueToLocation::TargetCallFramesEnum::Any) { |
| return true; |
| } |
| std::unique_ptr<V8StackTraceImpl> currentStack = captureStackTrace(true); |
| if (m_continueToLocationTargetCallFrames == |
| protocol::Debugger::ContinueToLocation::TargetCallFramesEnum::Current) { |
| return m_continueToLocationStack->isEqualIgnoringTopFrame( |
| currentStack.get()); |
| } |
| return true; |
| } |
| |
| void V8Debugger::clearContinueToLocation() { |
| if (m_continueToLocationBreakpointId == kNoBreakpointId) return; |
| v8::debug::RemoveBreakpoint(m_isolate, m_continueToLocationBreakpointId); |
| m_continueToLocationBreakpointId = kNoBreakpointId; |
| m_continueToLocationTargetCallFrames = String16(); |
| m_continueToLocationStack.reset(); |
| } |
| |
| void V8Debugger::handleProgramBreak( |
| v8::Local<v8::Context> pausedContext, v8::Local<v8::Value> exception, |
| const std::vector<v8::debug::BreakpointId>& breakpointIds, |
| bool isPromiseRejection, bool isUncaught) { |
| // Don't allow nested breaks. |
| if (isPaused()) return; |
| |
| int contextGroupId = m_inspector->contextGroupId(pausedContext); |
| if (m_targetContextGroupId && contextGroupId != m_targetContextGroupId) { |
| v8::debug::PrepareStep(m_isolate, v8::debug::StepOut); |
| return; |
| } |
| m_targetContextGroupId = 0; |
| if (m_stepIntoAsyncCallback) { |
| m_stepIntoAsyncCallback->sendFailure( |
| Response::Error("No async tasks were scheduled before pause.")); |
| m_stepIntoAsyncCallback.reset(); |
| } |
| m_breakRequested = false; |
| m_pauseOnAsyncCall = false; |
| m_taskWithScheduledBreak = nullptr; |
| m_taskWithScheduledBreakDebuggerId = String16(); |
| |
| bool scheduledOOMBreak = m_scheduledOOMBreak; |
| bool scheduledAssertBreak = m_scheduledAssertBreak; |
| bool hasAgents = false; |
| m_inspector->forEachSession( |
| contextGroupId, |
| [&scheduledOOMBreak, &hasAgents](V8InspectorSessionImpl* session) { |
| if (session->debuggerAgent()->acceptsPause(scheduledOOMBreak)) |
| hasAgents = true; |
| }); |
| if (!hasAgents) return; |
| |
| if (breakpointIds.size() == 1 && |
| breakpointIds[0] == m_continueToLocationBreakpointId) { |
| v8::Context::Scope contextScope(pausedContext); |
| if (!shouldContinueToCurrentLocation()) return; |
| } |
| clearContinueToLocation(); |
| |
| DCHECK(contextGroupId); |
| m_pausedContextGroupId = contextGroupId; |
| |
| m_inspector->forEachSession( |
| contextGroupId, [&pausedContext, &exception, &breakpointIds, |
| &isPromiseRejection, &isUncaught, &scheduledOOMBreak, |
| &scheduledAssertBreak](V8InspectorSessionImpl* session) { |
| if (session->debuggerAgent()->acceptsPause(scheduledOOMBreak)) { |
| session->debuggerAgent()->didPause( |
| InspectedContext::contextId(pausedContext), exception, |
| breakpointIds, isPromiseRejection, isUncaught, scheduledOOMBreak, |
| scheduledAssertBreak); |
| } |
| }); |
| { |
| v8::Context::Scope scope(pausedContext); |
| v8::Local<v8::Context> context = m_isolate->GetCurrentContext(); |
| CHECK(!context.IsEmpty() && |
| context != v8::debug::GetDebugContext(m_isolate)); |
| m_inspector->client()->runMessageLoopOnPause(contextGroupId); |
| m_pausedContextGroupId = 0; |
| } |
| m_inspector->forEachSession(contextGroupId, |
| [](V8InspectorSessionImpl* session) { |
| if (session->debuggerAgent()->enabled()) |
| session->debuggerAgent()->didContinue(); |
| }); |
| |
| if (m_scheduledOOMBreak) m_isolate->RestoreOriginalHeapLimit(); |
| m_scheduledOOMBreak = false; |
| m_scheduledAssertBreak = false; |
| } |
| |
| void V8Debugger::v8OOMCallback(void* data) { |
| V8Debugger* thisPtr = static_cast<V8Debugger*>(data); |
| thisPtr->m_isolate->IncreaseHeapLimitForDebugging(); |
| thisPtr->m_scheduledOOMBreak = true; |
| v8::Local<v8::Context> context = thisPtr->m_isolate->GetEnteredContext(); |
| DCHECK(!context.IsEmpty()); |
| thisPtr->setPauseOnNextStatement( |
| true, thisPtr->m_inspector->contextGroupId(context)); |
| } |
| |
| void V8Debugger::ScriptCompiled(v8::Local<v8::debug::Script> script, |
| bool is_live_edited, bool has_compile_error) { |
| int contextId; |
| if (!script->ContextId().To(&contextId)) return; |
| if (script->IsWasm()) { |
| WasmTranslation* wasmTranslation = &m_wasmTranslation; |
| m_inspector->forEachSession( |
| m_inspector->contextGroupId(contextId), |
| [&script, &wasmTranslation](V8InspectorSessionImpl* session) { |
| if (!session->debuggerAgent()->enabled()) return; |
| wasmTranslation->AddScript(script.As<v8::debug::WasmScript>(), |
| session->debuggerAgent()); |
| }); |
| } else if (m_ignoreScriptParsedEventsCounter == 0) { |
| v8::Isolate* isolate = m_isolate; |
| m_inspector->forEachSession( |
| m_inspector->contextGroupId(contextId), |
| [&isolate, &script, &has_compile_error, |
| &is_live_edited](V8InspectorSessionImpl* session) { |
| if (!session->debuggerAgent()->enabled()) return; |
| session->debuggerAgent()->didParseSource( |
| V8DebuggerScript::Create(isolate, script, is_live_edited), |
| !has_compile_error); |
| }); |
| } |
| } |
| |
| void V8Debugger::BreakProgramRequested( |
| v8::Local<v8::Context> pausedContext, v8::Local<v8::Object>, |
| v8::Local<v8::Value>, |
| const std::vector<v8::debug::BreakpointId>& break_points_hit) { |
| handleProgramBreak(pausedContext, v8::Local<v8::Value>(), break_points_hit); |
| } |
| |
| void V8Debugger::ExceptionThrown(v8::Local<v8::Context> pausedContext, |
| v8::Local<v8::Object>, |
| v8::Local<v8::Value> exception, |
| v8::Local<v8::Value> promise, |
| bool isUncaught) { |
| bool isPromiseRejection = promise->IsPromise(); |
| std::vector<v8::debug::BreakpointId> break_points_hit; |
| handleProgramBreak(pausedContext, exception, break_points_hit, |
| isPromiseRejection, isUncaught); |
| } |
| |
| bool V8Debugger::IsFunctionBlackboxed(v8::Local<v8::debug::Script> script, |
| const v8::debug::Location& start, |
| const v8::debug::Location& end) { |
| int contextId; |
| if (!script->ContextId().To(&contextId)) return false; |
| bool hasAgents = false; |
| bool allBlackboxed = true; |
| String16 scriptId = String16::fromInteger(script->Id()); |
| m_inspector->forEachSession( |
| m_inspector->contextGroupId(contextId), |
| [&hasAgents, &allBlackboxed, &scriptId, &start, |
| &end](V8InspectorSessionImpl* session) { |
| V8DebuggerAgentImpl* agent = session->debuggerAgent(); |
| if (!agent->enabled()) return; |
| hasAgents = true; |
| allBlackboxed &= agent->isFunctionBlackboxed(scriptId, start, end); |
| }); |
| return hasAgents && allBlackboxed; |
| } |
| |
| void V8Debugger::PromiseEventOccurred(v8::debug::PromiseDebugActionType type, |
| int id, bool isBlackboxed) { |
| // Async task events from Promises are given misaligned pointers to prevent |
| // from overlapping with other Blink task identifiers. |
| void* task = reinterpret_cast<void*>(id * 2 + 1); |
| switch (type) { |
| case v8::debug::kDebugAsyncFunctionPromiseCreated: |
| asyncTaskScheduledForStack("async function", task, true); |
| break; |
| case v8::debug::kDebugPromiseThen: |
| asyncTaskScheduledForStack("Promise.then", task, false); |
| if (!isBlackboxed) asyncTaskCandidateForStepping(task, true); |
| break; |
| case v8::debug::kDebugPromiseCatch: |
| asyncTaskScheduledForStack("Promise.catch", task, false); |
| if (!isBlackboxed) asyncTaskCandidateForStepping(task, true); |
| break; |
| case v8::debug::kDebugPromiseFinally: |
| asyncTaskScheduledForStack("Promise.finally", task, false); |
| if (!isBlackboxed) asyncTaskCandidateForStepping(task, true); |
| break; |
| case v8::debug::kDebugWillHandle: |
| asyncTaskStartedForStack(task); |
| asyncTaskStartedForStepping(task); |
| break; |
| case v8::debug::kDebugDidHandle: |
| asyncTaskFinishedForStack(task); |
| asyncTaskFinishedForStepping(task); |
| break; |
| } |
| } |
| |
| std::shared_ptr<AsyncStackTrace> V8Debugger::currentAsyncParent() { |
| return m_currentAsyncParent.empty() ? nullptr : m_currentAsyncParent.back(); |
| } |
| |
| V8StackTraceId V8Debugger::currentExternalParent() { |
| return m_currentExternalParent.empty() ? V8StackTraceId() |
| : m_currentExternalParent.back(); |
| } |
| |
| v8::MaybeLocal<v8::Value> V8Debugger::getTargetScopes( |
| v8::Local<v8::Context> context, v8::Local<v8::Value> value, |
| ScopeTargetKind kind) { |
| v8::Local<v8::Value> scopesValue; |
| std::unique_ptr<v8::debug::ScopeIterator> iterator; |
| switch (kind) { |
| case FUNCTION: |
| iterator = v8::debug::ScopeIterator::CreateForFunction( |
| m_isolate, v8::Local<v8::Function>::Cast(value)); |
| break; |
| case GENERATOR: |
| v8::Local<v8::debug::GeneratorObject> generatorObject = |
| v8::debug::GeneratorObject::Cast(value); |
| if (!generatorObject->IsSuspended()) return v8::MaybeLocal<v8::Value>(); |
| |
| iterator = v8::debug::ScopeIterator::CreateForGeneratorObject( |
| m_isolate, v8::Local<v8::Object>::Cast(value)); |
| break; |
| } |
| if (!iterator) return v8::MaybeLocal<v8::Value>(); |
| v8::Local<v8::Array> result = v8::Array::New(m_isolate); |
| if (!result->SetPrototype(context, v8::Null(m_isolate)).FromMaybe(false)) { |
| return v8::MaybeLocal<v8::Value>(); |
| } |
| |
| for (; !iterator->Done(); iterator->Advance()) { |
| v8::Local<v8::Object> scope = v8::Object::New(m_isolate); |
| if (!markAsInternal(context, scope, V8InternalValueType::kScope)) { |
| return v8::MaybeLocal<v8::Value>(); |
| } |
| String16 type = v8_inspector::scopeType(iterator->GetType()); |
| String16 name; |
| v8::Local<v8::Function> closure = iterator->GetFunction(); |
| if (!closure.IsEmpty()) { |
| name = toProtocolStringWithTypeCheck(closure->GetDebugName()); |
| } |
| v8::Local<v8::Object> object = iterator->GetObject(); |
| createDataProperty(context, scope, |
| toV8StringInternalized(m_isolate, "type"), |
| toV8String(m_isolate, type)); |
| createDataProperty(context, scope, |
| toV8StringInternalized(m_isolate, "name"), |
| toV8String(m_isolate, name)); |
| createDataProperty(context, scope, |
| toV8StringInternalized(m_isolate, "object"), object); |
| createDataProperty(context, result, result->Length(), scope); |
| } |
| if (!markAsInternal(context, v8::Local<v8::Array>::Cast(result), |
| V8InternalValueType::kScopeList)) |
| return v8::MaybeLocal<v8::Value>(); |
| return result; |
| } |
| |
| v8::MaybeLocal<v8::Value> V8Debugger::functionScopes( |
| v8::Local<v8::Context> context, v8::Local<v8::Function> function) { |
| return getTargetScopes(context, function, FUNCTION); |
| } |
| |
| v8::MaybeLocal<v8::Value> V8Debugger::generatorScopes( |
| v8::Local<v8::Context> context, v8::Local<v8::Value> generator) { |
| return getTargetScopes(context, generator, GENERATOR); |
| } |
| |
| v8::MaybeLocal<v8::Array> V8Debugger::internalProperties( |
| v8::Local<v8::Context> context, v8::Local<v8::Value> value) { |
| v8::Local<v8::Array> properties; |
| if (!v8::debug::GetInternalProperties(m_isolate, value).ToLocal(&properties)) |
| return v8::MaybeLocal<v8::Array>(); |
| if (value->IsFunction()) { |
| v8::Local<v8::Function> function = value.As<v8::Function>(); |
| v8::Local<v8::Object> location; |
| if (buildLocation(context, function->ScriptId(), |
| function->GetScriptLineNumber(), |
| function->GetScriptColumnNumber()) |
| .ToLocal(&location)) { |
| createDataProperty( |
| context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[FunctionLocation]]")); |
| createDataProperty(context, properties, properties->Length(), location); |
| } |
| if (function->IsGeneratorFunction()) { |
| createDataProperty(context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[IsGenerator]]")); |
| createDataProperty(context, properties, properties->Length(), |
| v8::True(m_isolate)); |
| } |
| } |
| v8::Local<v8::Array> entries; |
| if (collectionsEntries(context, value).ToLocal(&entries)) { |
| createDataProperty(context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[Entries]]")); |
| createDataProperty(context, properties, properties->Length(), entries); |
| } |
| if (value->IsGeneratorObject()) { |
| v8::Local<v8::Object> location; |
| if (generatorObjectLocation(context, value).ToLocal(&location)) { |
| createDataProperty( |
| context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[GeneratorLocation]]")); |
| createDataProperty(context, properties, properties->Length(), location); |
| } |
| v8::Local<v8::Value> scopes; |
| if (generatorScopes(context, value).ToLocal(&scopes)) { |
| createDataProperty(context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[Scopes]]")); |
| createDataProperty(context, properties, properties->Length(), scopes); |
| } |
| } |
| if (value->IsFunction()) { |
| v8::Local<v8::Function> function = value.As<v8::Function>(); |
| v8::Local<v8::Value> scopes; |
| if (functionScopes(context, function).ToLocal(&scopes)) { |
| createDataProperty(context, properties, properties->Length(), |
| toV8StringInternalized(m_isolate, "[[Scopes]]")); |
| createDataProperty(context, properties, properties->Length(), scopes); |
| } |
| } |
| return properties; |
| } |
| |
| v8::Local<v8::Array> V8Debugger::queryObjects(v8::Local<v8::Context> context, |
| v8::Local<v8::Object> prototype) { |
| v8::Isolate* isolate = context->GetIsolate(); |
| v8::PersistentValueVector<v8::Object> v8Objects(isolate); |
| MatchPrototypePredicate predicate(m_inspector, context, prototype); |
| v8::debug::QueryObjects(context, &predicate, &v8Objects); |
| |
| v8::MicrotasksScope microtasksScope(isolate, |
| v8::MicrotasksScope::kDoNotRunMicrotasks); |
| v8::Local<v8::Array> resultArray = v8::Array::New( |
| m_inspector->isolate(), static_cast<int>(v8Objects.Size())); |
| for (size_t i = 0; i < v8Objects.Size(); ++i) { |
| createDataProperty(context, resultArray, static_cast<int>(i), |
| v8Objects.Get(i)); |
| } |
| return resultArray; |
| } |
| |
| std::unique_ptr<V8StackTraceImpl> V8Debugger::createStackTrace( |
| v8::Local<v8::StackTrace> v8StackTrace) { |
| return V8StackTraceImpl::create(this, currentContextGroupId(), v8StackTrace, |
| V8StackTraceImpl::maxCallStackSizeToCapture); |
| } |
| |
| void V8Debugger::setAsyncCallStackDepth(V8DebuggerAgentImpl* agent, int depth) { |
| if (depth <= 0) |
| m_maxAsyncCallStackDepthMap.erase(agent); |
| else |
| m_maxAsyncCallStackDepthMap[agent] = depth; |
| |
| int maxAsyncCallStackDepth = 0; |
| for (const auto& pair : m_maxAsyncCallStackDepthMap) { |
| if (pair.second > maxAsyncCallStackDepth) |
| maxAsyncCallStackDepth = pair.second; |
| } |
| |
| if (m_maxAsyncCallStackDepth == maxAsyncCallStackDepth) return; |
| // TODO(dgozman): ideally, this should be per context group. |
| m_maxAsyncCallStackDepth = maxAsyncCallStackDepth; |
| m_inspector->client()->maxAsyncCallStackDepthChanged( |
| m_maxAsyncCallStackDepth); |
| if (!maxAsyncCallStackDepth) allAsyncTasksCanceled(); |
| } |
| |
| std::shared_ptr<AsyncStackTrace> V8Debugger::stackTraceFor( |
| int contextGroupId, const V8StackTraceId& id) { |
| if (debuggerIdFor(contextGroupId) != id.debugger_id) return nullptr; |
| auto it = m_storedStackTraces.find(id.id); |
| if (it == m_storedStackTraces.end()) return nullptr; |
| return it->second.lock(); |
| } |
| |
| V8StackTraceId V8Debugger::storeCurrentStackTrace( |
| const StringView& description) { |
| if (!m_maxAsyncCallStackDepth) return V8StackTraceId(); |
| |
| v8::HandleScope scope(m_isolate); |
| int contextGroupId = currentContextGroupId(); |
| if (!contextGroupId) return V8StackTraceId(); |
| |
| std::shared_ptr<AsyncStackTrace> asyncStack = |
| AsyncStackTrace::capture(this, contextGroupId, toString16(description), |
| V8StackTraceImpl::maxCallStackSizeToCapture); |
| if (!asyncStack) return V8StackTraceId(); |
| |
| uintptr_t id = AsyncStackTrace::store(this, asyncStack); |
| |
| m_allAsyncStacks.push_back(std::move(asyncStack)); |
| ++m_asyncStacksCount; |
| collectOldAsyncStacksIfNeeded(); |
| |
| asyncTaskCandidateForStepping(reinterpret_cast<void*>(id), false); |
| |
| return V8StackTraceId(id, debuggerIdFor(contextGroupId)); |
| } |
| |
| uintptr_t V8Debugger::storeStackTrace( |
| std::shared_ptr<AsyncStackTrace> asyncStack) { |
| uintptr_t id = ++m_lastStackTraceId; |
| m_storedStackTraces[id] = asyncStack; |
| return id; |
| } |
| |
| void V8Debugger::externalAsyncTaskStarted(const V8StackTraceId& parent) { |
| if (!m_maxAsyncCallStackDepth || parent.IsInvalid()) return; |
| m_currentExternalParent.push_back(parent); |
| m_currentAsyncParent.emplace_back(); |
| m_currentTasks.push_back(reinterpret_cast<void*>(parent.id)); |
| |
| if (m_breakRequested) return; |
| if (!m_taskWithScheduledBreakDebuggerId.isEmpty() && |
| reinterpret_cast<uintptr_t>(m_taskWithScheduledBreak) == parent.id && |
| m_taskWithScheduledBreakDebuggerId == |
| debuggerIdToString(parent.debugger_id)) { |
| v8::debug::DebugBreak(m_isolate); |
| } |
| } |
| |
| void V8Debugger::externalAsyncTaskFinished(const V8StackTraceId& parent) { |
| if (!m_maxAsyncCallStackDepth || m_currentExternalParent.empty()) return; |
| m_currentExternalParent.pop_back(); |
| m_currentAsyncParent.pop_back(); |
| DCHECK(m_currentTasks.back() == reinterpret_cast<void*>(parent.id)); |
| m_currentTasks.pop_back(); |
| |
| if (m_taskWithScheduledBreakDebuggerId.isEmpty() || |
| reinterpret_cast<uintptr_t>(m_taskWithScheduledBreak) != parent.id || |
| m_taskWithScheduledBreakDebuggerId != |
| debuggerIdToString(parent.debugger_id)) { |
| return; |
| } |
| m_taskWithScheduledBreak = nullptr; |
| m_taskWithScheduledBreakDebuggerId = String16(); |
| if (m_breakRequested) return; |
| v8::debug::CancelDebugBreak(m_isolate); |
| } |
| |
| void V8Debugger::asyncTaskScheduled(const StringView& taskName, void* task, |
| bool recurring) { |
| asyncTaskScheduledForStack(toString16(taskName), task, recurring); |
| asyncTaskCandidateForStepping(task, true); |
| } |
| |
| void V8Debugger::asyncTaskCanceled(void* task) { |
| asyncTaskCanceledForStack(task); |
| asyncTaskCanceledForStepping(task); |
| } |
| |
| void V8Debugger::asyncTaskStarted(void* task) { |
| asyncTaskStartedForStack(task); |
| asyncTaskStartedForStepping(task); |
| } |
| |
| void V8Debugger::asyncTaskFinished(void* task) { |
| asyncTaskFinishedForStepping(task); |
| asyncTaskFinishedForStack(task); |
| } |
| |
| void V8Debugger::asyncTaskScheduledForStack(const String16& taskName, |
| void* task, bool recurring) { |
| if (!m_maxAsyncCallStackDepth) return; |
| v8::HandleScope scope(m_isolate); |
| std::shared_ptr<AsyncStackTrace> asyncStack = |
| AsyncStackTrace::capture(this, currentContextGroupId(), taskName, |
| V8StackTraceImpl::maxCallStackSizeToCapture); |
| if (asyncStack) { |
| m_asyncTaskStacks[task] = asyncStack; |
| if (recurring) m_recurringTasks.insert(task); |
| m_allAsyncStacks.push_back(std::move(asyncStack)); |
| ++m_asyncStacksCount; |
| collectOldAsyncStacksIfNeeded(); |
| } |
| } |
| |
| void V8Debugger::asyncTaskCanceledForStack(void* task) { |
| if (!m_maxAsyncCallStackDepth) return; |
| m_asyncTaskStacks.erase(task); |
| m_recurringTasks.erase(task); |
| } |
| |
| void V8Debugger::asyncTaskStartedForStack(void* task) { |
| if (!m_maxAsyncCallStackDepth) return; |
| // Needs to support following order of events: |
| // - asyncTaskScheduled |
| // <-- attached here --> |
| // - asyncTaskStarted |
| // - asyncTaskCanceled <-- canceled before finished |
| // <-- async stack requested here --> |
| // - asyncTaskFinished |
| m_currentTasks.push_back(task); |
| AsyncTaskToStackTrace::iterator stackIt = m_asyncTaskStacks.find(task); |
| if (stackIt != m_asyncTaskStacks.end()) { |
| m_currentAsyncParent.push_back(stackIt->second.lock()); |
| } else { |
| m_currentAsyncParent.emplace_back(); |
| } |
| m_currentExternalParent.emplace_back(); |
| } |
| |
| void V8Debugger::asyncTaskFinishedForStack(void* task) { |
| if (!m_maxAsyncCallStackDepth) return; |
| // We could start instrumenting half way and the stack is empty. |
| if (!m_currentTasks.size()) return; |
| DCHECK(m_currentTasks.back() == task); |
| m_currentTasks.pop_back(); |
| |
| m_currentAsyncParent.pop_back(); |
| m_currentExternalParent.pop_back(); |
| |
| if (m_recurringTasks.find(task) == m_recurringTasks.end()) { |
| asyncTaskCanceledForStack(task); |
| } |
| } |
| |
| void V8Debugger::asyncTaskCandidateForStepping(void* task, bool isLocal) { |
| int contextGroupId = currentContextGroupId(); |
| if (m_pauseOnAsyncCall && contextGroupId) { |
| if (isLocal) { |
| m_scheduledAsyncCall = v8_inspector::V8StackTraceId( |
| reinterpret_cast<uintptr_t>(task), std::make_pair(0, 0)); |
| } else { |
| m_scheduledAsyncCall = v8_inspector::V8StackTraceId( |
| reinterpret_cast<uintptr_t>(task), debuggerIdFor(contextGroupId)); |
| } |
| breakProgram(m_targetContextGroupId); |
| m_scheduledAsyncCall = v8_inspector::V8StackTraceId(); |
| return; |
| } |
| if (!m_stepIntoAsyncCallback) return; |
| DCHECK(m_targetContextGroupId); |
| if (contextGroupId != m_targetContextGroupId) return; |
| m_taskWithScheduledBreak = task; |
| v8::debug::ClearStepping(m_isolate); |
| m_stepIntoAsyncCallback->sendSuccess(); |
| m_stepIntoAsyncCallback.reset(); |
| } |
| |
| void V8Debugger::asyncTaskStartedForStepping(void* task) { |
| if (m_breakRequested) return; |
| // TODO(kozyatinskiy): we should search task in async chain to support |
| // blackboxing. |
| if (m_taskWithScheduledBreakDebuggerId.isEmpty() && |
| task == m_taskWithScheduledBreak) { |
| v8::debug::DebugBreak(m_isolate); |
| } |
| } |
| |
| void V8Debugger::asyncTaskFinishedForStepping(void* task) { |
| if (!m_taskWithScheduledBreakDebuggerId.isEmpty() || |
| task != m_taskWithScheduledBreak) { |
| return; |
| } |
| m_taskWithScheduledBreak = nullptr; |
| if (m_breakRequested) return; |
| v8::debug::CancelDebugBreak(m_isolate); |
| } |
| |
| void V8Debugger::asyncTaskCanceledForStepping(void* task) { |
| if (!m_taskWithScheduledBreakDebuggerId.isEmpty() || |
| task != m_taskWithScheduledBreak) |
| return; |
| m_taskWithScheduledBreak = nullptr; |
| } |
| |
| void V8Debugger::allAsyncTasksCanceled() { |
| m_asyncTaskStacks.clear(); |
| m_recurringTasks.clear(); |
| m_currentAsyncParent.clear(); |
| m_currentExternalParent.clear(); |
| m_currentTasks.clear(); |
| |
| m_framesCache.clear(); |
| m_allAsyncStacks.clear(); |
| m_asyncStacksCount = 0; |
| } |
| |
| void V8Debugger::muteScriptParsedEvents() { |
| ++m_ignoreScriptParsedEventsCounter; |
| } |
| |
| void V8Debugger::unmuteScriptParsedEvents() { |
| --m_ignoreScriptParsedEventsCounter; |
| DCHECK_GE(m_ignoreScriptParsedEventsCounter, 0); |
| } |
| |
| std::unique_ptr<V8StackTraceImpl> V8Debugger::captureStackTrace( |
| bool fullStack) { |
| if (!m_isolate->InContext()) return nullptr; |
| |
| v8::HandleScope handles(m_isolate); |
| int contextGroupId = currentContextGroupId(); |
| if (!contextGroupId) return nullptr; |
| |
| int stackSize = 1; |
| if (fullStack) { |
| stackSize = V8StackTraceImpl::maxCallStackSizeToCapture; |
| } else { |
| m_inspector->forEachSession( |
| contextGroupId, [&stackSize](V8InspectorSessionImpl* session) { |
| if (session->runtimeAgent()->enabled()) |
| stackSize = V8StackTraceImpl::maxCallStackSizeToCapture; |
| }); |
| } |
| return V8StackTraceImpl::capture(this, contextGroupId, stackSize); |
| } |
| |
| int V8Debugger::currentContextGroupId() { |
| if (!m_isolate->InContext()) return 0; |
| return m_inspector->contextGroupId(m_isolate->GetCurrentContext()); |
| } |
| |
| void V8Debugger::collectOldAsyncStacksIfNeeded() { |
| if (m_asyncStacksCount <= m_maxAsyncCallStacks) return; |
| int halfOfLimitRoundedUp = |
| m_maxAsyncCallStacks / 2 + m_maxAsyncCallStacks % 2; |
| while (m_asyncStacksCount > halfOfLimitRoundedUp) { |
| m_allAsyncStacks.pop_front(); |
| --m_asyncStacksCount; |
| } |
| cleanupExpiredWeakPointers(m_asyncTaskStacks); |
| cleanupExpiredWeakPointers(m_storedStackTraces); |
| for (auto it = m_recurringTasks.begin(); it != m_recurringTasks.end();) { |
| if (m_asyncTaskStacks.find(*it) == m_asyncTaskStacks.end()) { |
| it = m_recurringTasks.erase(it); |
| } else { |
| ++it; |
| } |
| } |
| cleanupExpiredWeakPointers(m_framesCache); |
| } |
| |
| std::shared_ptr<StackFrame> V8Debugger::symbolize( |
| v8::Local<v8::StackFrame> v8Frame) { |
| auto it = m_framesCache.end(); |
| int frameId = 0; |
| if (m_maxAsyncCallStackDepth) { |
| frameId = v8::debug::GetStackFrameId(v8Frame); |
| it = m_framesCache.find(frameId); |
| } |
| if (it != m_framesCache.end() && it->second.lock()) return it->second.lock(); |
| std::shared_ptr<StackFrame> frame(new StackFrame(v8Frame)); |
| // TODO(clemensh): Figure out a way to do this translation only right before |
| // sending the stack trace over wire. |
| if (v8Frame->IsWasm()) frame->translate(&m_wasmTranslation); |
| if (m_maxAsyncCallStackDepth) { |
| m_framesCache[frameId] = frame; |
| } |
| return frame; |
| } |
| |
| void V8Debugger::setMaxAsyncTaskStacksForTest(int limit) { |
| m_maxAsyncCallStacks = 0; |
| collectOldAsyncStacksIfNeeded(); |
| m_maxAsyncCallStacks = limit; |
| } |
| |
| std::pair<int64_t, int64_t> V8Debugger::debuggerIdFor(int contextGroupId) { |
| auto it = m_contextGroupIdToDebuggerId.find(contextGroupId); |
| if (it != m_contextGroupIdToDebuggerId.end()) return it->second; |
| std::pair<int64_t, int64_t> debuggerId( |
| v8::debug::GetNextRandomInt64(m_isolate), |
| v8::debug::GetNextRandomInt64(m_isolate)); |
| if (!debuggerId.first && !debuggerId.second) ++debuggerId.first; |
| m_contextGroupIdToDebuggerId.insert( |
| it, std::make_pair(contextGroupId, debuggerId)); |
| m_serializedDebuggerIdToDebuggerId.insert( |
| std::make_pair(debuggerIdToString(debuggerId), debuggerId)); |
| return debuggerId; |
| } |
| |
| std::pair<int64_t, int64_t> V8Debugger::debuggerIdFor( |
| const String16& serializedDebuggerId) { |
| auto it = m_serializedDebuggerIdToDebuggerId.find(serializedDebuggerId); |
| if (it != m_serializedDebuggerIdToDebuggerId.end()) return it->second; |
| return std::make_pair(0, 0); |
| } |
| |
| void V8Debugger::dumpAsyncTaskStacksStateForTest() { |
| fprintf(stdout, "Async stacks count: %d\n", m_asyncStacksCount); |
| fprintf(stdout, "Scheduled async tasks: %zu\n", m_asyncTaskStacks.size()); |
| fprintf(stdout, "Recurring async tasks: %zu\n", m_recurringTasks.size()); |
| fprintf(stdout, "\n"); |
| } |
| |
| } // namespace v8_inspector |