/* * Copyright (C) 2008-2019 Apple Inc. All rights reserved. * Copyright (C) 1999-2001 Harri Porten (porten@kde.org) * Copyright (C) 2001 Peter Kelly (pmk@post.com) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "config.h" #include "Debugger.h" #include "CodeBlock.h" #include "DebuggerCallFrame.h" #include "DebuggerScope.h" #include "HeapIterationScope.h" #include "JSCInlines.h" #include "MarkedSpaceInlines.h" #include "VMEntryScope.h" #include #include #include #include #include namespace JSC { class DebuggerPausedScope { public: DebuggerPausedScope(Debugger& debugger) : m_debugger(debugger) { ASSERT(!m_debugger.m_currentDebuggerCallFrame); } ~DebuggerPausedScope() { if (m_debugger.m_currentDebuggerCallFrame) { m_debugger.m_currentDebuggerCallFrame->invalidate(); m_debugger.m_currentDebuggerCallFrame = nullptr; } } private: Debugger& m_debugger; }; // This is very similar to SetForScope, but that cannot be used // as the m_isPaused field uses only one bit. class TemporaryPausedState { public: TemporaryPausedState(Debugger& debugger) : m_debugger(debugger) { ASSERT(!m_debugger.m_isPaused); m_debugger.m_isPaused = true; } ~TemporaryPausedState() { m_debugger.m_isPaused = false; } private: Debugger& m_debugger; }; Debugger::TemporarilyDisableExceptionBreakpoints::TemporarilyDisableExceptionBreakpoints(Debugger& debugger) : m_debugger(debugger) { } Debugger::TemporarilyDisableExceptionBreakpoints::~TemporarilyDisableExceptionBreakpoints() { restore(); } void Debugger::TemporarilyDisableExceptionBreakpoints::replace() { if (m_debugger.m_pauseOnAllExceptionsBreakpoint) m_pauseOnAllExceptionsBreakpoint = WTFMove(m_debugger.m_pauseOnAllExceptionsBreakpoint); if (m_debugger.m_pauseOnUncaughtExceptionsBreakpoint) m_pauseOnUncaughtExceptionsBreakpoint = WTFMove(m_debugger.m_pauseOnUncaughtExceptionsBreakpoint); } void Debugger::TemporarilyDisableExceptionBreakpoints::restore() { if (m_pauseOnAllExceptionsBreakpoint) m_debugger.m_pauseOnAllExceptionsBreakpoint = WTFMove(m_pauseOnAllExceptionsBreakpoint); if (m_pauseOnUncaughtExceptionsBreakpoint) m_debugger.m_pauseOnUncaughtExceptionsBreakpoint = WTFMove(m_pauseOnUncaughtExceptionsBreakpoint); } Debugger::ProfilingClient::~ProfilingClient() { } Debugger::Debugger(VM& vm) : m_vm(vm) , m_pauseAtNextOpportunity(false) , m_pastFirstExpressionInStatement(false) , m_isPaused(false) , m_breakpointsActivated(false) , m_hasHandlerForExceptionCallback(false) , m_suppressAllPauses(false) , m_steppingMode(SteppingModeDisabled) , m_reasonForPause(NotPaused) , m_lastExecutedLine(UINT_MAX) , m_lastExecutedSourceID(noSourceID) , m_pausingBreakpointID(noBreakpointID) { } Debugger::~Debugger() { HashSet::iterator end = m_globalObjects.end(); for (HashSet::iterator it = m_globalObjects.begin(); it != end; ++it) (*it)->setDebugger(nullptr); } void Debugger::attach(JSGlobalObject* globalObject) { ASSERT(!globalObject->debugger()); globalObject->setDebugger(this); m_globalObjects.add(globalObject); m_vm.setShouldBuildPCToCodeOriginMapping(); // Call `sourceParsed` after iterating because it will execute JavaScript in Web Inspector. HashSet> sourceProviders; { HeapIterationScope iterationScope(m_vm.heap); m_vm.heap.objectSpace().forEachLiveCell(iterationScope, [&] (HeapCell* heapCell, HeapCell::Kind kind) { if (isJSCellKind(kind)) { auto* cell = static_cast(heapCell); if (auto* function = jsDynamicCast(cell->vm(), cell)) { if (function->scope()->globalObject() == globalObject && function->executable()->isFunctionExecutable() && !function->isHostOrBuiltinFunction()) sourceProviders.add(jsCast(function->executable())->source().provider()); } } return IterationStatus::Continue; }); } for (auto& sourceProvider : sourceProviders) sourceParsed(globalObject, sourceProvider.get(), -1, nullString()); } void Debugger::detach(JSGlobalObject* globalObject, ReasonForDetach reason) { // If we're detaching from the currently executing global object, manually tear down our // stack, since we won't get further debugger callbacks to do so. Also, resume execution, // since there's no point in staying paused once a window closes. // We know there is an entry scope, otherwise, m_currentCallFrame would be null. VM& vm = globalObject->vm(); JSLockHolder locker(vm); if (m_isPaused && m_currentCallFrame && vm.entryScope->globalObject() == globalObject) { m_currentCallFrame = nullptr; m_pauseOnCallFrame = nullptr; continueProgram(); } ASSERT(m_globalObjects.contains(globalObject)); m_globalObjects.remove(globalObject); // If the globalObject is destructing, then its CodeBlocks will also be // destructed. There is no need to do the debugger requests clean up, and // it is not safe to access those CodeBlocks at this time anyway. if (reason != GlobalObjectIsDestructing) clearDebuggerRequests(globalObject); globalObject->setDebugger(nullptr); if (m_globalObjects.isEmpty()) clearParsedData(); } bool Debugger::isAttached(JSGlobalObject* globalObject) { return globalObject->debugger() == this; } class Debugger::SetSteppingModeFunctor { public: SetSteppingModeFunctor(Debugger* debugger, SteppingMode mode) : m_debugger(debugger) , m_mode(mode) { } void operator()(CodeBlock* codeBlock) const { if (m_debugger == codeBlock->globalObject()->debugger()) { if (m_mode == SteppingModeEnabled) codeBlock->setSteppingMode(CodeBlock::SteppingModeEnabled); else codeBlock->setSteppingMode(CodeBlock::SteppingModeDisabled); } } private: Debugger* m_debugger; SteppingMode m_mode; }; void Debugger::setSteppingMode(SteppingMode mode) { if (mode == m_steppingMode) return; m_vm.heap.completeAllJITPlans(); m_steppingMode = mode; SetSteppingModeFunctor functor(this, mode); m_vm.heap.forEachCodeBlock(functor); } void Debugger::registerCodeBlock(CodeBlock* codeBlock) { applyBreakpoints(codeBlock); if (isStepping()) codeBlock->setSteppingMode(CodeBlock::SteppingModeEnabled); } void Debugger::setClient(Client* client) { ASSERT(!!m_client != !!client); m_client = client; } void Debugger::addObserver(Observer& observer) { bool wasEmpty = m_observers.isEmpty(); m_observers.add(&observer); if (wasEmpty) attachDebugger(); } void Debugger::removeObserver(Observer& observer, bool isBeingDestroyed) { m_observers.remove(&observer); if (m_observers.isEmpty()) detachDebugger(isBeingDestroyed); } bool Debugger::canDispatchFunctionToObservers() const { return !m_dispatchingFunctionToObservers && !m_observers.isEmpty(); } void Debugger::dispatchFunctionToObservers(Function func) { if (!canDispatchFunctionToObservers()) return; SetForScope change(m_dispatchingFunctionToObservers, true); for (auto* observer : copyToVector(m_observers)) func(*observer); } void Debugger::setProfilingClient(ProfilingClient* client) { ASSERT(!!m_profilingClient != !!client); m_profilingClient = client; } void Debugger::sourceParsed(JSGlobalObject* globalObject, SourceProvider* sourceProvider, int errorLine, const String& errorMessage) { // Preemptively check whether we can dispatch so that we don't do any unnecessary allocations. if (!canDispatchFunctionToObservers()) return; if (errorLine != -1) { auto sourceURL = sourceProvider->sourceURL(); auto data = sourceProvider->source().toString(); auto firstLine = sourceProvider->startPosition().m_line.oneBasedInt(); dispatchFunctionToObservers([&] (Observer& observer) { observer.failedToParseSource(sourceURL, data, firstLine, errorLine, errorMessage); }); return; } JSC::SourceID sourceID = sourceProvider->asID(); // FIXME: Web Inspector: Simplify Debugger::Script to use SourceProvider Debugger::Script script; script.sourceProvider = sourceProvider; script.url = sourceProvider->sourceURL(); script.source = sourceProvider->source().toString(); script.startLine = sourceProvider->startPosition().m_line.zeroBasedInt(); script.startColumn = sourceProvider->startPosition().m_column.zeroBasedInt(); script.isContentScript = isContentScript(globalObject); script.sourceURL = sourceProvider->sourceURLDirective(); script.sourceMappingURL = sourceProvider->sourceMappingURLDirective(); int sourceLength = script.source.length(); int lineCount = 1; int lastLineStart = 0; for (int i = 0; i < sourceLength; ++i) { if (script.source[i] == '\n') { lineCount += 1; lastLineStart = i + 1; } } script.endLine = script.startLine + lineCount - 1; if (lineCount == 1) script.endColumn = script.startColumn + sourceLength; else script.endColumn = sourceLength - lastLineStart; dispatchFunctionToObservers([&] (Observer& observer) { observer.didParseSource(sourceID, script); }); } Seconds Debugger::willEvaluateScript() { return m_profilingClient->willEvaluateScript(); } void Debugger::didEvaluateScript(Seconds startTime, ProfilingReason reason) { m_profilingClient->didEvaluateScript(startTime, reason); } void Debugger::toggleBreakpoint(CodeBlock* codeBlock, Breakpoint& breakpoint, BreakpointState enabledOrNot) { ASSERT(breakpoint.isResolved()); ScriptExecutable* executable = codeBlock->ownerExecutable(); SourceID sourceID = static_cast(executable->sourceID()); if (breakpoint.sourceID() != sourceID) return; unsigned startLine = executable->firstLine(); unsigned startColumn = executable->startColumn(); unsigned endLine = executable->lastLine(); unsigned endColumn = executable->endColumn(); // Inspector breakpoint line and column values are zero-based but the executable // and CodeBlock line and column values are one-based. unsigned line = breakpoint.lineNumber() + 1; Optional column; if (breakpoint.columnNumber()) column = breakpoint.columnNumber() + 1; if (line < startLine || line > endLine) return; if (column) { if (line == startLine && column < startColumn) return; if (line == endLine && column > endColumn) return; } if (!codeBlock->hasOpDebugForLineAndColumn(line, column)) return; if (enabledOrNot == BreakpointEnabled) codeBlock->addBreakpoint(1); else codeBlock->removeBreakpoint(1); } void Debugger::applyBreakpoints(CodeBlock* codeBlock) { for (auto& breakpoint : m_breakpoints) toggleBreakpoint(codeBlock, breakpoint, BreakpointEnabled); } class Debugger::ToggleBreakpointFunctor { public: ToggleBreakpointFunctor(Debugger* debugger, Breakpoint& breakpoint, BreakpointState enabledOrNot) : m_debugger(debugger) , m_breakpoint(breakpoint) , m_enabledOrNot(enabledOrNot) { } void operator()(CodeBlock* codeBlock) const { if (m_debugger == codeBlock->globalObject()->debugger()) m_debugger->toggleBreakpoint(codeBlock, m_breakpoint, m_enabledOrNot); } private: Debugger* m_debugger; Breakpoint& m_breakpoint; BreakpointState m_enabledOrNot; }; void Debugger::toggleBreakpoint(Breakpoint& breakpoint, Debugger::BreakpointState enabledOrNot) { m_vm.heap.completeAllJITPlans(); ToggleBreakpointFunctor functor(this, breakpoint, enabledOrNot); m_vm.heap.forEachCodeBlock(functor); } void Debugger::recompileAllJSFunctions() { m_vm.deleteAllCode(PreventCollectionAndDeleteAllCode); } DebuggerParseData& Debugger::debuggerParseData(SourceID sourceID, SourceProvider* provider) { auto iter = m_parseDataMap.find(sourceID); if (iter != m_parseDataMap.end()) return iter->value; DebuggerParseData parseData; gatherDebuggerParseDataForSource(m_vm, provider, parseData); auto result = m_parseDataMap.add(sourceID, parseData); return result.iterator->value; } bool Debugger::resolveBreakpoint(Breakpoint& breakpoint, SourceProvider* sourceProvider) { RELEASE_ASSERT(!breakpoint.isResolved()); ASSERT(breakpoint.isLinked()); // FIXME: Web Inspector: Adopt TextPosition in Inspector to avoid oneBasedInt/zeroBasedInt ambiguity // Inspector breakpoint line and column values are zero-based but the executable // and CodeBlock line values are one-based while column is zero-based. int line = breakpoint.lineNumber() + 1; int column = breakpoint.columnNumber(); // Account for a