Fixes a crash in JavaScriptDebugServer::returnEvent when debugging
[WebKit-https.git] / WebCore / page / JavaScriptDebugServer.cpp
index bd324aa..ffa5672 100644 (file)
 #include "JavaScriptDebugServer.h"
 
 #include "DOMWindow.h"
+#include "EventLoop.h"
 #include "Frame.h"
+#include "FrameTree.h"
+#include "FrameView.h"
 #include "JSDOMWindow.h"
+#include "JavaScriptCallFrame.h"
 #include "JavaScriptDebugListener.h"
+#include "kjs_proxy.h"
 #include "Page.h"
+#include "PageGroup.h"
+#include "PluginView.h"
+#include "ScrollView.h"
+#include "Widget.h"
+#include <wtf/MainThread.h>
 
 using namespace KJS;
 
@@ -49,16 +59,22 @@ JavaScriptDebugServer& JavaScriptDebugServer::shared()
 
 JavaScriptDebugServer::JavaScriptDebugServer()
     : m_callingListeners(false)
+    , m_pauseOnExceptions(false)
+    , m_pauseOnNextStatement(false)
+    , m_paused(false)
+    , m_pauseOnExecState(0)
 {
 }
 
 JavaScriptDebugServer::~JavaScriptDebugServer()
 {
+    deleteAllValues(m_pageListenersMap);
+    deleteAllValues(m_breakpoints);
 }
 
 void JavaScriptDebugServer::addListener(JavaScriptDebugListener* listener)
 {
-    if (m_listeners.isEmpty())
+    if (!hasListeners())
         Page::setDebuggerForAllPages(this);
 
     m_listeners.add(listener);
@@ -67,86 +83,466 @@ void JavaScriptDebugServer::addListener(JavaScriptDebugListener* listener)
 void JavaScriptDebugServer::removeListener(JavaScriptDebugListener* listener)
 {
     m_listeners.remove(listener);
-    if (m_listeners.isEmpty())
+    if (!hasListeners()) {
         Page::setDebuggerForAllPages(0);
+        resume();
+    }
+}
+
+void JavaScriptDebugServer::addListener(JavaScriptDebugListener* listener, Page* page)
+{
+    ASSERT_ARG(page, page);
+
+    if (!hasListeners())
+        Page::setDebuggerForAllPages(this);
+
+    pair<PageListenersMap::iterator, bool> result = m_pageListenersMap.add(page, 0);
+    if (result.second)
+        result.first->second = new ListenerSet;
+    ListenerSet* listeners = result.first->second;
+
+    listeners->add(listener);
+}
+
+void JavaScriptDebugServer::removeListener(JavaScriptDebugListener* listener, Page* page)
+{
+    ASSERT_ARG(page, page);
+
+    PageListenersMap::iterator it = m_pageListenersMap.find(page);
+    if (it == m_pageListenersMap.end())
+        return;
+
+    ListenerSet* listeners = it->second;
+
+    listeners->remove(listener);
+
+    if (listeners->isEmpty()) {
+        m_pageListenersMap.remove(it);
+        delete listeners;
+    }
+
+    if (!hasListeners()) {
+        Page::setDebuggerForAllPages(0);
+        resume();
+    }
 }
 
 void JavaScriptDebugServer::pageCreated(Page* page)
 {
-    if (m_listeners.isEmpty())
+    if (!hasListeners())
         return;
 
     page->setDebugger(this);
 }
 
-static void dispatchDidParseSource(const ListenerSet& listeners, ExecState* state, const String& source, int startingLineNumber, const String& sourceURL, int sourceID)
+bool JavaScriptDebugServer::hasListenersInterestedInPage(Page* page)
+{
+    ASSERT_ARG(page, page);
+
+    if (!m_listeners.isEmpty())
+        return true;
+
+    return m_pageListenersMap.contains(page);
+}
+
+void JavaScriptDebugServer::addBreakpoint(int sourceID, unsigned lineNumber)
+{
+    HashSet<unsigned>* lines = m_breakpoints.get(sourceID);
+    if (!lines) {
+        lines = new HashSet<unsigned>;
+        m_breakpoints.set(sourceID, lines);
+    }
+
+    lines->add(lineNumber);
+}
+
+void JavaScriptDebugServer::removeBreakpoint(int sourceID, unsigned lineNumber)
+{
+    HashSet<unsigned>* lines = m_breakpoints.get(sourceID);
+    if (!lines)
+        return;
+
+    lines->remove(lineNumber);
+
+    if (!lines->isEmpty())
+        return;
+
+    m_breakpoints.remove(sourceID);
+    delete lines;
+}
+
+bool JavaScriptDebugServer::hasBreakpoint(int sourceID, unsigned lineNumber) const
+{
+    HashSet<unsigned>* lines = m_breakpoints.get(sourceID);
+    if (!lines)
+        return false;
+    return lines->contains(lineNumber);
+}
+
+void JavaScriptDebugServer::clearBreakpoints()
+{
+    deleteAllValues(m_breakpoints);
+    m_breakpoints.clear();
+}
+
+void JavaScriptDebugServer::setPauseOnExceptions(bool pause)
+{
+    m_pauseOnExceptions = pause;
+}
+
+void JavaScriptDebugServer::pauseOnNextStatement()
+{
+    m_pauseOnNextStatement = true;
+}
+
+void JavaScriptDebugServer::resume()
+{
+    m_paused = false;
+}
+
+void JavaScriptDebugServer::stepIntoStatement()
+{
+    if (!m_paused)
+        return;
+
+    resume();
+
+    m_pauseOnNextStatement = true;
+}
+
+void JavaScriptDebugServer::stepOverStatement()
+{
+    if (!m_paused)
+        return;
+
+    resume();
+
+    if (m_currentCallFrame)
+        m_pauseOnExecState = m_currentCallFrame->execState();
+    else
+        m_pauseOnExecState = 0;
+}
+
+void JavaScriptDebugServer::stepOutOfFunction()
+{
+    if (!m_paused)
+        return;
+
+    resume();
+
+    if (m_currentCallFrame && m_currentCallFrame->caller())
+        m_pauseOnExecState = m_currentCallFrame->caller()->execState();
+    else
+        m_pauseOnExecState = 0;
+}
+
+JavaScriptCallFrame* JavaScriptDebugServer::currentCallFrame()
+{
+    if (!m_paused)
+        return 0;
+    return m_currentCallFrame.get();
+}
+
+static void dispatchDidParseSource(const ListenerSet& listeners, const UString& source, int startingLineNumber, const UString& sourceURL, int sourceID)
 {
     Vector<JavaScriptDebugListener*> copy;
     copyToVector(listeners, copy);
     for (size_t i = 0; i < copy.size(); ++i)
-        copy[i]->didParseSource(state, source, startingLineNumber, sourceURL, sourceID);
+        copy[i]->didParseSource(source, startingLineNumber, sourceURL, sourceID);
 }
 
-static void dispatchFailedToParseSource(const ListenerSet& listeners, ExecState* state, const String& source, int startingLineNumber, const String& sourceURL, int errorLine, const String& errorMessage)
+static void dispatchFailedToParseSource(const ListenerSet& listeners, const UString& source, int startingLineNumber, const UString& sourceURL, int errorLine, const UString& errorMessage)
 {
     Vector<JavaScriptDebugListener*> copy;
     copyToVector(listeners, copy);
     for (size_t i = 0; i < copy.size(); ++i)
-        copy[i]->failedToParseSource(state, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+        copy[i]->failedToParseSource(source, startingLineNumber, sourceURL, errorLine, errorMessage);
+}
+
+static Page* toPage(ExecState* exec)
+{
+    ASSERT_ARG(exec, exec);
+
+    JSDOMWindow* window = asJSDOMWindow(exec->dynamicGlobalObject());
+    ASSERT(window);
+
+    return window->impl()->frame()->page();
 }
 
-bool JavaScriptDebugServer::sourceParsed(ExecState* state, int sourceID, const UString& sourceURL, const UString& source, int startingLineNumber, int errorLine, const UString& errorMessage)
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+static unsigned s_callDepth = 0;
+#endif
+
+bool JavaScriptDebugServer::sourceParsed(ExecState* exec, int sourceID, const UString& sourceURL, const UString& source, int startingLineNumber, int errorLine, const UString& errorMessage)
 {
     if (m_callingListeners)
         return true;
+
+    Page* page = toPage(exec);
+    if (!page)
+        return true;
+
     m_callingListeners = true;
 
-    ASSERT(!m_listeners.isEmpty());
-    if (errorLine == -1)
-        dispatchDidParseSource(m_listeners, state, source, startingLineNumber, sourceURL, sourceID);
-    else
-        dispatchFailedToParseSource(m_listeners, state, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+    ASSERT(hasListeners());
+
+    bool isError = errorLine != -1;
+
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+    printf("source: ");
+    for(unsigned i = 0; i < s_callDepth; ++i)
+        printf(" ");
+    printf("%d: '%s' exec: %p (caller: %p) source: %d\n", s_callDepth, sourceURL.ascii(), exec, exec->callingExecState(), sourceID);
+#endif
+
+    if (!m_listeners.isEmpty()) {
+        if (isError)
+            dispatchFailedToParseSource(m_listeners, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+        else
+            dispatchDidParseSource(m_listeners, source, startingLineNumber, sourceURL, sourceID);
+    }
+
+    if (ListenerSet* pageListeners = m_pageListenersMap.get(page)) {
+        ASSERT(!pageListeners->isEmpty());
+        if (isError)
+            dispatchFailedToParseSource(*pageListeners, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+        else
+            dispatchDidParseSource(*pageListeners, source, startingLineNumber, sourceURL, sourceID);
+    }
 
     m_callingListeners = false;
     return true;
 }
 
-void JavaScriptDebugServer::dispatchFunctionToListeners(JavaScriptExecutionCallback callback, ExecState* state, int sourceID, int lineNumber)
+static void dispatchFunctionToListeners(const ListenerSet& listeners, JavaScriptDebugServer::JavaScriptExecutionCallback callback)
+{
+    Vector<JavaScriptDebugListener*> copy;
+    copyToVector(listeners, copy);
+    for (size_t i = 0; i < copy.size(); ++i)
+        (copy[i]->*callback)();
+}
+
+void JavaScriptDebugServer::dispatchFunctionToListeners(JavaScriptExecutionCallback callback, ExecState* exec)
 {
     if (m_callingListeners)
         return;
+
+    Page* page = toPage(exec);
+    if (!page)
+        return;
+
     m_callingListeners = true;
 
-    ASSERT(!m_listeners.isEmpty());
-    Vector<JavaScriptDebugListener*> copy;
-    copyToVector(m_listeners, copy);
-    for (size_t i = 0; i < copy.size(); ++i)
-        (copy[i]->*callback)(state, sourceID, lineNumber);
+    ASSERT(hasListeners());
+
+    WebCore::dispatchFunctionToListeners(m_listeners, callback);
+    if (ListenerSet* pageListeners = m_pageListenersMap.get(page)) {
+        ASSERT(!pageListeners->isEmpty());
+        WebCore::dispatchFunctionToListeners(*pageListeners, callback);
+    }
 
     m_callingListeners = false;
 }
 
-bool JavaScriptDebugServer::callEvent(ExecState* state, int sourceID, int lineNumber, JSObject*, const List&)
+void JavaScriptDebugServer::setJavaScriptPaused(const PageGroup& pageGroup, bool paused)
+{
+    setMainThreadCallbacksPaused(paused);
+
+    const HashSet<Page*>& pages = pageGroup.pages();
+
+    HashSet<Page*>::const_iterator end = pages.end();
+    for (HashSet<Page*>::const_iterator it = pages.begin(); it != end; ++it)
+        setJavaScriptPaused(*it, false);
+}
+
+void JavaScriptDebugServer::setJavaScriptPaused(Page* page, bool paused)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::didEnterCallFrame, state, sourceID, lineNumber);
+    ASSERT_ARG(page, page);
+
+    page->setDefersLoading(paused);
+
+    for (Frame* frame = page->mainFrame(); frame; frame = frame->tree()->traverseNext())
+        setJavaScriptPaused(frame, paused);
+}
+
+void JavaScriptDebugServer::setJavaScriptPaused(Frame* frame, bool paused)
+{
+    ASSERT_ARG(frame, frame);
+
+    if (!frame->scriptProxy()->isEnabled())
+        return;
+
+    frame->scriptProxy()->setPaused(paused);
+
+    if (JSDOMWindow* window = toJSDOMWindow(frame)) {
+        if (paused)
+            m_pausedTimeouts.set(frame, window->pauseTimeouts());
+        else
+            window->resumeTimeouts(m_pausedTimeouts.take(frame));
+    }
+
+    setJavaScriptPaused(frame->view(), paused);
+}
+
+void JavaScriptDebugServer::setJavaScriptPaused(FrameView* view, bool paused)
+{
+#if !PLATFORM(MAC)
+    if (!view)
+        return;
+
+    HashSet<Widget*>* children = static_cast<ScrollView*>(view)->children();
+    ASSERT(children);
+
+    HashSet<Widget*>::iterator end = children->end();
+    for (HashSet<Widget*>::iterator it = children->begin(); it != end; ++it) {
+        Widget* widget = *it;
+        if (!widget->isPluginView())
+            continue;
+        static_cast<PluginView*>(widget)->setJavaScriptPaused(paused);
+    }
+#endif
+}
+
+void JavaScriptDebugServer::pauseIfNeeded(ExecState* exec, int sourceID, int lineNumber)
+{
+    if (m_paused)
+        return;
+
+    Page* page = toPage(exec);
+    if (!page || !hasListenersInterestedInPage(page))
+        return;
+
+    bool pauseNow = m_pauseOnNextStatement;
+    if (!pauseNow && m_pauseOnExecState)
+        pauseNow = (m_pauseOnExecState == exec);
+    if (!pauseNow && lineNumber > 0)
+        pauseNow = hasBreakpoint(sourceID, lineNumber);
+    if (!pauseNow)
+        return;
+
+    m_pauseOnExecState = 0;
+    m_pauseOnNextStatement = false;
+    m_paused = true;
+
+    dispatchFunctionToListeners(&JavaScriptDebugListener::didPause, exec);
+
+    setJavaScriptPaused(page->group(), true);
+
+    EventLoop loop;
+    while (m_paused && !loop.ended())
+        loop.cycle();
+
+    setJavaScriptPaused(page->group(), false);
+
+    m_paused = false;
+}
+
+static inline void updateCurrentCallFrame(RefPtr<JavaScriptCallFrame>& currentCallFrame, ExecState* exec, int sourceID, int lineNumber, ExecState*& pauseExecState)
+{
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+    const char* action = 0;
+#endif
+
+    if (currentCallFrame) {
+        if (currentCallFrame->execState() == exec) {
+            // Same call frame, just update the current line.
+            currentCallFrame->setLine(lineNumber);
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+            action = "  same";
+#endif
+        } else if (currentCallFrame->execState() == exec->callingExecState()) {
+            // Create a new call frame, and make the caller the previous call frame.
+            currentCallFrame = JavaScriptCallFrame::create(exec, currentCallFrame, sourceID, lineNumber);
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+            action = "  call";
+            ++s_callDepth;
+#endif
+        } else {
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+            action = "return";
+#endif
+            // The current call frame isn't the same and it isn't the caller of a new call frame,
+            // so it might be a previous call frame (returning from a function). Or it is a stale call
+            // frame from the previous execution of global code. Walk up the caller chain until we find
+            // the current exec state. If the current exec state is found, the current call frame will be
+            // set to null (and a new one will be created below.)
+            while (currentCallFrame && currentCallFrame->execState() != exec) {
+                if (currentCallFrame->execState() == pauseExecState) {
+                    // The current call frame matches the pause exec state (used for step over.)
+                    // Since we are returning up the call stack, update the pause exec state to match.
+                    // This makes stepping over a return statement act like a step out.
+                    if (currentCallFrame->caller())
+                        pauseExecState = currentCallFrame->caller()->execState();
+                    else
+                        pauseExecState = 0;
+                }
+
+                // Invalidate the call frame since it's ExecState is stale now.
+                currentCallFrame->invalidate();
+                currentCallFrame = currentCallFrame->caller();
+
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+                if (s_callDepth)
+                    --s_callDepth;
+#endif
+            }
+
+            if (currentCallFrame)
+                currentCallFrame->setLine(lineNumber);
+        }
+    }
+
+    if (!currentCallFrame) {
+        // Create a new call frame with no caller, this is likely global code.
+        currentCallFrame = JavaScriptCallFrame::create(exec, 0, sourceID, lineNumber);
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+        action = "   new";
+#endif
+    }
+
+#ifdef DEBUG_DEBUGGER_CALLBACKS
+    printf("%s: ", action);
+    for(unsigned i = 0; i < s_callDepth; ++i)
+        printf(" ");
+    printf("%d: at exec: %p (caller: %p, pause: %p) source: %d line: %d\n", s_callDepth, exec, exec->callingExecState(), pauseExecState, sourceID, lineNumber);
+#endif
+}
+
+bool JavaScriptDebugServer::callEvent(ExecState* exec, int sourceID, int lineNumber, JSObject*, const List&)
+{
+    if (m_paused)
+        return true;
+    updateCurrentCallFrame(m_currentCallFrame, exec, sourceID, lineNumber, m_pauseOnExecState);
+    pauseIfNeeded(exec, sourceID, lineNumber);
     return true;
 }
 
-bool JavaScriptDebugServer::atStatement(ExecState* state, int sourceID, int firstLine, int)
+bool JavaScriptDebugServer::atStatement(ExecState* exec, int sourceID, int firstLine, int)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::willExecuteStatement, state, sourceID, firstLine);
+    if (m_paused)
+        return true;
+    updateCurrentCallFrame(m_currentCallFrame, exec, sourceID, firstLine, m_pauseOnExecState);
+    pauseIfNeeded(exec, sourceID, firstLine);
     return true;
 }
 
-bool JavaScriptDebugServer::returnEvent(ExecState* state, int sourceID, int lineNumber, JSObject*)
+bool JavaScriptDebugServer::returnEvent(ExecState* exec, int sourceID, int lineNumber, JSObject*)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::willLeaveCallFrame, state, sourceID, lineNumber);
+    if (m_paused)
+        return true;
+    updateCurrentCallFrame(m_currentCallFrame, exec, sourceID, lineNumber, m_pauseOnExecState);
+    pauseIfNeeded(exec, sourceID, lineNumber);
     return true;
 }
 
-bool JavaScriptDebugServer::exception(ExecState* state, int sourceID, int lineNumber, JSValue*)
+bool JavaScriptDebugServer::exception(ExecState* exec, int sourceID, int lineNumber, JSValue*)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::exceptionWasRaised, state, sourceID, lineNumber);
+    if (m_paused)
+        return true;
+    updateCurrentCallFrame(m_currentCallFrame, exec, sourceID, lineNumber, m_pauseOnExecState);
+    if (m_pauseOnExceptions)
+        m_pauseOnNextStatement = true;
+    pauseIfNeeded(exec, sourceID, lineNumber);
     return true;
 }