Fixes a crash in JavaScriptDebugServer::returnEvent when debugging
[WebKit-https.git] / WebCore / page / JavaScriptDebugServer.cpp
index beb387c..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,12 +59,17 @@ 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)
@@ -68,8 +83,10 @@ void JavaScriptDebugServer::addListener(JavaScriptDebugListener* listener)
 void JavaScriptDebugServer::removeListener(JavaScriptDebugListener* listener)
 {
     m_listeners.remove(listener);
-    if (!hasListeners())
+    if (!hasListeners()) {
         Page::setDebuggerForAllPages(0);
+        resume();
+    }
 }
 
 void JavaScriptDebugServer::addListener(JavaScriptDebugListener* listener, Page* page)
@@ -104,8 +121,10 @@ void JavaScriptDebugServer::removeListener(JavaScriptDebugListener* listener, Pa
         delete listeners;
     }
 
-    if (!hasListeners())
+    if (!hasListeners()) {
         Page::setDebuggerForAllPages(0);
+        resume();
+    }
 }
 
 void JavaScriptDebugServer::pageCreated(Page* page)
@@ -116,20 +135,128 @@ void JavaScriptDebugServer::pageCreated(Page* page)
     page->setDebugger(this);
 }
 
-static void dispatchDidParseSource(const ListenerSet& listeners, ExecState* exec, const UString& source, int startingLineNumber, const UString& 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(exec, source, startingLineNumber, sourceURL, sourceID);
+        copy[i]->didParseSource(source, startingLineNumber, sourceURL, sourceID);
 }
 
-static void dispatchFailedToParseSource(const ListenerSet& listeners, ExecState* exec, const UString& source, int startingLineNumber, const UString& sourceURL, int errorLine, const UString& 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(exec, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+        copy[i]->failedToParseSource(source, startingLineNumber, sourceURL, errorLine, errorMessage);
 }
 
 static Page* toPage(ExecState* exec)
@@ -142,6 +269,10 @@ static Page* toPage(ExecState* exec)
     return window->impl()->frame()->page();
 }
 
+#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)
@@ -157,34 +288,41 @@ bool JavaScriptDebugServer::sourceParsed(ExecState* exec, int sourceID, const US
 
     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, exec, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+            dispatchFailedToParseSource(m_listeners, source, startingLineNumber, sourceURL, errorLine, errorMessage);
         else
-            dispatchDidParseSource(m_listeners, exec, source, startingLineNumber, sourceURL, sourceID);
+            dispatchDidParseSource(m_listeners, source, startingLineNumber, sourceURL, sourceID);
     }
 
     if (ListenerSet* pageListeners = m_pageListenersMap.get(page)) {
         ASSERT(!pageListeners->isEmpty());
         if (isError)
-            dispatchFailedToParseSource(*pageListeners, exec, source, startingLineNumber, sourceURL, errorLine, errorMessage);
+            dispatchFailedToParseSource(*pageListeners, source, startingLineNumber, sourceURL, errorLine, errorMessage);
         else
-            dispatchDidParseSource(*pageListeners, exec, source, startingLineNumber, sourceURL, sourceID);
+            dispatchDidParseSource(*pageListeners, source, startingLineNumber, sourceURL, sourceID);
     }
 
     m_callingListeners = false;
     return true;
 }
 
-static void dispatchFunctionToListeners(const ListenerSet& listeners, JavaScriptDebugServer::JavaScriptExecutionCallback callback, ExecState* exec, 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)(exec, sourceID, lineNumber);
+        (copy[i]->*callback)();
 }
 
-void JavaScriptDebugServer::dispatchFunctionToListeners(JavaScriptExecutionCallback callback, ExecState* exec, int sourceID, int lineNumber)
+void JavaScriptDebugServer::dispatchFunctionToListeners(JavaScriptExecutionCallback callback, ExecState* exec)
 {
     if (m_callingListeners)
         return;
@@ -197,36 +335,214 @@ void JavaScriptDebugServer::dispatchFunctionToListeners(JavaScriptExecutionCallb
 
     ASSERT(hasListeners());
 
-    WebCore::dispatchFunctionToListeners(m_listeners, callback, exec, sourceID, lineNumber);
+    WebCore::dispatchFunctionToListeners(m_listeners, callback);
     if (ListenerSet* pageListeners = m_pageListenersMap.get(page)) {
         ASSERT(!pageListeners->isEmpty());
-        WebCore::dispatchFunctionToListeners(*pageListeners, callback, exec, sourceID, lineNumber);
+        WebCore::dispatchFunctionToListeners(*pageListeners, callback);
     }
 
     m_callingListeners = false;
 }
 
+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)
+{
+    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&)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::didEnterCallFrame, exec, sourceID, lineNumber);
+    if (m_paused)
+        return true;
+    updateCurrentCallFrame(m_currentCallFrame, exec, sourceID, lineNumber, m_pauseOnExecState);
+    pauseIfNeeded(exec, sourceID, lineNumber);
     return true;
 }
 
 bool JavaScriptDebugServer::atStatement(ExecState* exec, int sourceID, int firstLine, int)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::willExecuteStatement, exec, 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* exec, int sourceID, int lineNumber, JSObject*)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::willLeaveCallFrame, exec, 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* exec, int sourceID, int lineNumber, JSValue*)
 {
-    dispatchFunctionToListeners(&JavaScriptDebugListener::exceptionWasRaised, exec, 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;
 }