diff options
22 files changed, 2292 insertions, 89 deletions
diff --git a/chrome/browser/debugger/debugger.vcproj b/chrome/browser/debugger/debugger.vcproj index 38c32fd..2ef5241 100644 --- a/chrome/browser/debugger/debugger.vcproj +++ b/chrome/browser/debugger/debugger.vcproj @@ -150,6 +150,18 @@ > </File> <File + RelativePath=".\debugger_host.h" + > + </File> + <File + RelativePath=".\debugger_host_impl.cpp" + > + </File> + <File + RelativePath=".\debugger_host_impl.h" + > + </File> + <File RelativePath=".\debugger_io.h" > </File> diff --git a/chrome/browser/debugger/debugger_contents.cc b/chrome/browser/debugger/debugger_contents.cc index 2b3e998..37cb1d0 100644 --- a/chrome/browser/debugger/debugger_contents.cc +++ b/chrome/browser/debugger/debugger_contents.cc @@ -78,33 +78,32 @@ class DebuggerHTMLSource : public ChromeURLDataManager::DataSource { class DebuggerHandler : public DOMMessageHandler { public: explicit DebuggerHandler(DOMUIHost* host) { - host->RegisterMessageCallback("command", - NewCallback(this, &DebuggerHandler::HandleCommand)); + host->RegisterMessageCallback("DebuggerHostMessage", + NewCallback(this, &DebuggerHandler::HandleDebuggerHostMessage)); } - void HandleCommand(const Value* content) { - // Extract the parameters out of the input list. + void HandleDebuggerHostMessage(const Value* content) { if (!content || !content->IsType(Value::TYPE_LIST)) { NOTREACHED(); return; } const ListValue* args = static_cast<const ListValue*>(content); - if (args->GetSize() != 1) { + if (args->GetSize() < 1) { NOTREACHED(); return; } - std::wstring command; - Value* value = NULL; - if (!args->Get(0, &value) || !value->GetAsString(&command)) { + +#ifndef CHROME_DEBUGGER_DISABLED + DebuggerWrapper* wrapper = g_browser_process->debugger_wrapper(); + DebuggerHost* debugger_host = wrapper->GetDebugger(); + if (!debugger_host) { NOTREACHED(); return; } -#ifndef CHROME_DEBUGGER_DISABLED - DebuggerWrapper* wrapper = g_browser_process->debugger_wrapper(); - DebuggerShell* shell = wrapper->GetDebugger(); - shell->ProcessCommand(command); + debugger_host->OnDebuggerHostMsg(args); #endif } + private: DISALLOW_EVIL_CONSTRUCTORS(DebuggerHandler); }; @@ -127,8 +126,6 @@ void DebuggerContents::AttachMessageHandlers() { // static bool DebuggerContents::IsDebuggerUrl(const GURL& url) { - if (url.SchemeIs("chrome-resource") && url.host() == "debugger") - return true; - return false; + return (url.SchemeIs("chrome-resource") && url.host() == "inspector"); } diff --git a/chrome/browser/debugger/debugger_host.h b/chrome/browser/debugger/debugger_host.h new file mode 100644 index 0000000..bfb2efa --- /dev/null +++ b/chrome/browser/debugger/debugger_host.h @@ -0,0 +1,43 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// A part of browser-side server debugger exposed to DebuggerWrapper. + +#ifndef CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_H_ +#define CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_H_ + +#include "base/basictypes.h" +#include "base/thread.h" + +class ListValue; + +class DebuggerHost : public base::RefCountedThreadSafe<DebuggerHost> { + public: + DebuggerHost() {}; + virtual ~DebuggerHost() {}; + + // call before other methods + virtual void Start() = 0; + + // A message from the V8 debugger in the renderer being debugged via + // RenderViewHost + virtual void DebugMessage(const std::wstring& msg) = 0; + // We've been successfully attached to a renderer. + virtual void OnDebugAttach() = 0; + // The renderer we're attached to is gone. + virtual void OnDebugDisconnect() = 0; + + virtual void DidDisconnect() = 0; + virtual void DidConnect() {} + virtual void ProcessCommand(const std::wstring& data) {} + + // Handles messages from debugger UI. + virtual void OnDebuggerHostMsg(const ListValue* args) {} + private: + + DISALLOW_COPY_AND_ASSIGN(DebuggerHost); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_H_ + diff --git a/chrome/browser/debugger/debugger_host_impl.cpp b/chrome/browser/debugger/debugger_host_impl.cpp new file mode 100644 index 0000000..c2eb41d --- /dev/null +++ b/chrome/browser/debugger/debugger_host_impl.cpp @@ -0,0 +1,188 @@ +// Copyright (c) 2006-2008 The Chromium 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 "chrome/browser/debugger/debugger_host_impl.h" + +#include "base/json_reader.h" +#include "base/string_util.h" +#include "chrome/browser/debugger/debugger_io.h" +#include "chrome/browser/debugger/debugger_wrapper.h" +#include "chrome/browser/render_view_host.h" +#include "chrome/browser/web_contents.h" +#include "chrome/common/notification_service.h" + +class TabContentsReference : public NotificationObserver { + public: + TabContentsReference(TabContents *c) : navigation_controller_(NULL) { + navigation_controller_ = c->controller(); + + NotificationService* service = NotificationService::current(); + DCHECK(service); + service->AddObserver(this, NOTIFY_TAB_CLOSING, + Source<NavigationController>(navigation_controller_)); + observing_ = true; + } + + virtual ~TabContentsReference() { + StopObserving(); + } + + // NotificationObserver impl + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + StopObserving(); + navigation_controller_ = NULL; + } + + TabContents* GetTabContents() { + if (navigation_controller_) { + return navigation_controller_->active_contents(); + } else { + return NULL; + } + } + private: + void StopObserving() { + if (observing_ && navigation_controller_) { + NotificationService* service = NotificationService::current(); + DCHECK(service); + service->RemoveObserver( + this, + NOTIFY_TAB_CLOSING, + Source<NavigationController>(navigation_controller_)); + observing_ = false; + } + } + + NavigationController* navigation_controller_; + bool observing_; + + DISALLOW_COPY_AND_ASSIGN(TabContentsReference); +}; + + +DebuggerHostImpl::DebuggerHostImpl(DebuggerInputOutput* io) + : io_(io), + debugger_ready_(true) { +} + +DebuggerHostImpl::~DebuggerHostImpl() { + io_->Stop(); + io_ = NULL; +} + +void DebuggerHostImpl::Start() { + io_->Start(this); +} + +void DebuggerHostImpl::Debug(TabContents* tab) { + tab_reference_.reset(new TabContentsReference(tab)); +} + +void DebuggerHostImpl::DebugMessage(const std::wstring& msg) { + + Value* msg_value = NULL; + if (!JSONReader::Read(WideToUTF8(msg), &msg_value, false)) { + msg_value = Value::CreateStringValue(L"Message parse error!"); + } + ListValue* argv = new ListValue; + argv->Append(msg_value); + io_->CallFunctionInPage(L"response", argv); +} + +void DebuggerHostImpl::OnDebugAttach() { + std::wstring title; + const TabContents* t = GetTabContentsBeingDebugged(); + if (t) { + title = t->GetTitle(); + } + + ListValue* argv = new ListValue; + argv->Append(Value::CreateStringValue(title)); + io_->CallFunctionInPage(L"on_attach", argv); +} + +void DebuggerHostImpl::OnDebugDisconnect() { + ListValue* argv = new ListValue; + io_->CallFunctionInPage(L"on_disconnect", argv); +} + +void DebuggerHostImpl::DidDisconnect() { + // TODO(yurys): just send Detach from here? + ListValue* argv = new ListValue; + io_->CallFunctionInPage(L"exit", argv); +} + +void DebuggerHostImpl::OnDebuggerHostMsg(const ListValue* args) { + if (args->GetSize() < 1) { + NOTREACHED(); + return; + } + std::wstring methodName; + Value* value = NULL; + if (!args->Get(0, &value) || !value->GetAsString(&methodName)) { + NOTREACHED(); + return; + } + + const TabContents* t = GetTabContentsBeingDebugged(); + if (t == NULL) { + NOTREACHED(); + return; + } + const WebContents* web = t->AsWebContents(); + if (web == NULL) { + NOTREACHED(); + return; + } + RenderViewHost* host = web->render_view_host(); + + if (methodName == L"attach") { + host->DebugAttach(); + } else if (methodName == L"detach") { + host->DebugDetach(); + } else if (methodName == L"debugBreak") { + std::wstring force; + Value* value = NULL; + if (!args->Get(1, &value) || !value->GetAsString(&force)) { + NOTREACHED(); + return; + } + host->DebugBreak(force == L"true"); + } else if (methodName == L"sendToDebugger") { + std::wstring cmd; + Value* value = NULL; + if (!args->Get(1, &value) || !value->GetAsString(&cmd)) { + NOTREACHED(); + return; + } + host->DebugCommand(cmd); + + } else if (methodName == L"setDebuggerReady") { + std::wstring ready; + Value* value = NULL; + if (!args->Get(1, &value) || !value->GetAsString(&ready)) { + NOTREACHED(); + return; + } + io_->SetDebuggerReady(ready == L"true"); + } else if (methodName == L"setDebuggerBreak") { + std::wstring brk; + Value* value = NULL; + if (!args->Get(1, &value) || !value->GetAsString(&brk)) { + NOTREACHED(); + return; + } + io_->SetDebuggerBreak(brk == L"true"); + } +} + +TabContents* DebuggerHostImpl::GetTabContentsBeingDebugged() const { + if (tab_reference_ != NULL) { + return tab_reference_->GetTabContents(); + } else { + return NULL; + } +} diff --git a/chrome/browser/debugger/debugger_host_impl.h b/chrome/browser/debugger/debugger_host_impl.h new file mode 100644 index 0000000..ce2c3bb --- /dev/null +++ b/chrome/browser/debugger/debugger_host_impl.h @@ -0,0 +1,51 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_IMPL_H__ +#define CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_IMPL_H__ + +#include "base/scoped_ptr.h" +#include "chrome/browser/debugger/debugger_host.h" + +class DebuggerInputOutput; +class TabContents; +class TabContentsReference; + +class DebuggerHostImpl : public DebuggerHost { + public: + DebuggerHostImpl(DebuggerInputOutput *io); + virtual ~DebuggerHostImpl(); + + virtual void Start(); + + // Start debugging the specified tab + virtual void Debug(TabContents* tab); + // A message from the V8 debugger in the renderer being debugged via + // RenderViewHost + virtual void DebugMessage(const std::wstring& msg); + // We've been successfully attached to a renderer. + virtual void OnDebugAttach(); + // The renderer we're attached to is gone. + virtual void OnDebugDisconnect(); + + virtual void DidDisconnect(); + + // Handles messages from debugger UI. + virtual void OnDebuggerHostMsg(const ListValue* args); + + private: + + TabContents* GetTabContentsBeingDebugged() const; + + scoped_refptr<DebuggerInputOutput> io_; + // reference to the tab being debugged by this instance + scoped_ptr<TabContentsReference> tab_reference_; + // If the debugger is ready to process another command or is busy. + bool debugger_ready_; + + DISALLOW_COPY_AND_ASSIGN(DebuggerHostImpl); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEBUGGER_HOST_IMPL_H__ + diff --git a/chrome/browser/debugger/debugger_io.h b/chrome/browser/debugger/debugger_io.h index 1e4a5f4..b2ea381 100644 --- a/chrome/browser/debugger/debugger_io.h +++ b/chrome/browser/debugger/debugger_io.h @@ -6,11 +6,13 @@ #define CHROME_BROWSER_DEBUGGER_DEBUGGER_IO_H_ #include <string> +#include <vector> #include "base/basictypes.h" #include "base/ref_counted.h" -class DebuggerShell; +class DebuggerHost; +class ListValue; class DebuggerInputOutput : public base::RefCountedThreadSafe<DebuggerInputOutput> { @@ -19,7 +21,7 @@ public: virtual ~DebuggerInputOutput() {} // Called when Debugger is ready to begin. - virtual void Start(DebuggerShell* debugger) { debugger_ = debugger; } + virtual void Start(DebuggerHost* debugger) { debugger_ = debugger; } // Called when Debugger is shutting down virtual void Stop() {} @@ -39,8 +41,12 @@ public: // is running, and true when the page is stopped at a breakpoint virtual void SetDebuggerBreak(bool brk) {} + // sends message to debugger UI page in order to invoke JS function in it + virtual void CallFunctionInPage(const std::wstring& name, + ListValue* argv) {} + protected: - DebuggerShell* debugger_; + DebuggerHost* debugger_; private: DISALLOW_COPY_AND_ASSIGN(DebuggerInputOutput); diff --git a/chrome/browser/debugger/debugger_io_socket.cc b/chrome/browser/debugger/debugger_io_socket.cc index eb3431d..5280600 100644 --- a/chrome/browser/debugger/debugger_io_socket.cc +++ b/chrome/browser/debugger/debugger_io_socket.cc @@ -23,7 +23,7 @@ DebuggerInputOutputSocket::DebuggerInputOutputSocket(int port) io_loop_ = g_browser_process->io_thread()->message_loop(); } -void DebuggerInputOutputSocket::Start(DebuggerShell* debugger) { +void DebuggerInputOutputSocket::Start(DebuggerHost* debugger) { DebuggerInputOutput::Start(debugger); io_loop_->PostTask(FROM_HERE, NewRunnableMethod( this, &DebuggerInputOutputSocket::StartListening)); @@ -57,7 +57,7 @@ void DebuggerInputOutputSocket::DidAccept(ListenSocket *server, connection_ = connection; connection_->AddRef(); ui_loop_->PostTask(FROM_HERE, NewRunnableMethod( - debugger_, &DebuggerShell::DidConnect)); + debugger_, &DebuggerHost::DidConnect)); } else { delete connection; } @@ -114,7 +114,7 @@ void DebuggerInputOutputSocket::DidRead(ListenSocket *connection, if (connection == connection_) { const std::wstring wstr = UTF8ToWide(data); ui_loop_->PostTask(FROM_HERE, NewRunnableMethod( - debugger_, &DebuggerShell::ProcessCommand, wstr)); + debugger_, &DebuggerHost::ProcessCommand, wstr)); } else { // TODO(erikkay): assert? } diff --git a/chrome/browser/debugger/debugger_io_socket.h b/chrome/browser/debugger/debugger_io_socket.h index 884a5d5..551e14d 100644 --- a/chrome/browser/debugger/debugger_io_socket.h +++ b/chrome/browser/debugger/debugger_io_socket.h @@ -8,7 +8,7 @@ #include "chrome/browser/debugger/debugger_io.h" #include "net/base/telnet_server.h" -class DebuggerShell; +class DebuggerHost; class MessageLoop; // Interaction with the underlying Socket object MUST happen in the IO thread. @@ -35,7 +35,7 @@ public: virtual void Output(const std::string& out); virtual void OutputLine(const std::string& out); virtual void OutputPrompt(const std::string& prompt); - virtual void Start(DebuggerShell* debugger); + virtual void Start(DebuggerHost* debugger); // Stop must be called prior to this object being released, so that cleanup // can happen in the IO thread. virtual void Stop(); diff --git a/chrome/browser/debugger/debugger_shell.h b/chrome/browser/debugger/debugger_shell.h index 6068393..bdb5d8a 100644 --- a/chrome/browser/debugger/debugger_shell.h +++ b/chrome/browser/debugger/debugger_shell.h @@ -17,6 +17,7 @@ #include "base/basictypes.h" #include "base/ref_counted.h" +#include "chrome/browser/debugger/debugger_host.h" #ifdef CHROME_DEBUGGER_DISABLED @@ -44,7 +45,7 @@ class DebuggerInputOutput; class MessageLoop; class TabContents; -class DebuggerShell : public base::RefCountedThreadSafe<DebuggerShell> { +class DebuggerShell : public DebuggerHost { public: DebuggerShell(DebuggerInputOutput *io); virtual ~DebuggerShell(); diff --git a/chrome/browser/debugger/debugger_view.cc b/chrome/browser/debugger/debugger_view.cc index 6b59cd6..5fd62f1 100644 --- a/chrome/browser/debugger/debugger_view.cc +++ b/chrome/browser/debugger/debugger_view.cc @@ -70,6 +70,12 @@ void DebuggerView::SetOutputViewReady() { Output(*i); } pending_output_.clear(); + + for (std::vector<std::string>::const_iterator i = pending_events_.begin(); + i != pending_events_.end(); ++i) { + ExecuteJavascript(*i); + } + pending_events_.clear(); } void DebuggerView::Output(const std::string& out) { @@ -81,11 +87,10 @@ void DebuggerView::Output(const std::wstring& out) { pending_output_.push_back(out); return; } - Value* str_value = Value::CreateStringValue(out); - std::string json; - JSONWriter::Write(str_value, false, &json); - const std::string js = StringPrintf("appendText(%s)", json.c_str()); - ExecuteJavascript(js); + + DictionaryValue* body = new DictionaryValue; + body->Set(L"text", Value::CreateStringValue(out)); + SendEventToPage(L"appendText", body); } void DebuggerView::OnInit() { @@ -100,15 +105,14 @@ void DebuggerView::OnInit() { web_container_->SetTabContents(web_contents_); web_contents_->render_view_host()->AllowDOMUIBindings(); - GURL contents("chrome-resource://debugger/"); + GURL contents("chrome-resource://inspector/debugger.html"); + web_contents_->controller()->LoadURL(contents, GURL(), PageTransition::START_PAGE); } void DebuggerView::OnShow() { web_contents_->Focus(); - if (output_ready_) - ExecuteJavascript("focusOnCommandLine()"); } void DebuggerView::OnClose() { @@ -116,12 +120,6 @@ void DebuggerView::OnClose() { web_contents_->CloseContents(); } -void DebuggerView::SetDebuggerBreak(bool is_broken) { - const std::string js = - StringPrintf("setDebuggerBreak(%s)", is_broken ? "true" : "false"); - ExecuteJavascript(js); -} - void DebuggerView::OpenURLFromTab(TabContents* source, const GURL& url, const GURL& referrer, @@ -131,6 +129,26 @@ void DebuggerView::OpenURLFromTab(TabContents* source, transition); } + +void DebuggerView::SendEventToPage(const std::wstring& name, + Value* body) { + DictionaryValue msg; + msg.SetString(L"type", L"event"); + msg.SetString(L"event", name); + msg.Set(L"body", body); + + std::string json; + JSONWriter::Write(&msg, false, &json); + + const std::string js = + StringPrintf("DebuggerIPC.onMessageReceived(%s)", json.c_str()); + if (output_ready_) { + ExecuteJavascript(js); + } else { + pending_events_.push_back(js); + } +} + void DebuggerView::ExecuteJavascript(const std::string& js) { web_contents_->render_view_host()->ExecuteJavascriptInWebFrame(L"", UTF8ToWide(js)); diff --git a/chrome/browser/debugger/debugger_view.h b/chrome/browser/debugger/debugger_view.h index 5a7f775..9603777 100644 --- a/chrome/browser/debugger/debugger_view.h +++ b/chrome/browser/debugger/debugger_view.h @@ -18,6 +18,7 @@ class DebuggerView; class DebuggerWindow; class TabContentsContainerView; +class Value; class WebContents; class DebuggerView : public views::View, @@ -38,9 +39,6 @@ class DebuggerView : public views::View, // Called when the window is being closed. void OnClose(); - // Called when the debugger hits a breakpoint or continues. - void SetDebuggerBreak(bool is_broken); - void SetOutputViewReady(); // Overridden from views::View: @@ -81,6 +79,10 @@ class DebuggerView : public views::View, virtual void UpdateTargetURL(TabContents* source, const GURL& url) {} virtual bool CanBlur() const { return false; } + // To pass messages from DebuggerHost to debugger UI. + // Note that this method will take ownership of body. + void SendEventToPage(const std::wstring& name, Value* body); + private: void ExecuteJavascript(const std::string& js); @@ -88,6 +90,7 @@ class DebuggerView : public views::View, WebContents* web_contents_; TabContentsContainerView* web_container_; std::vector<std::wstring> pending_output_; + std::vector<std::string> pending_events_; bool output_ready_; DISALLOW_EVIL_CONSTRUCTORS(DebuggerView); diff --git a/chrome/browser/debugger/debugger_window.cc b/chrome/browser/debugger/debugger_window.cc index 731c907..e340bff 100644 --- a/chrome/browser/debugger/debugger_window.cc +++ b/chrome/browser/debugger/debugger_window.cc @@ -5,7 +5,7 @@ #include "base/string_util.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/constrained_window.h" -#include "chrome/browser/debugger/debugger_shell.h" +#include "chrome/browser/debugger/debugger_host_impl.h" #include "chrome/browser/debugger/debugger_view.h" #include "chrome/browser/debugger/debugger_window.h" #include "chrome/browser/debugger/debugger_wrapper.h" @@ -42,7 +42,7 @@ void DebuggerWindow::Show(TabContents* tab) { view_->OnShow(); debugger_ready_ = true; debugger_break_ = false; - DebuggerShell* debugger = new DebuggerShell(this); + DebuggerHostImpl* debugger = new DebuggerHostImpl(this); DebuggerWrapper* wrapper = g_browser_process->debugger_wrapper(); if (!wrapper) { g_browser_process->InitDebuggerWrapper(0); @@ -95,7 +95,7 @@ void DebuggerWindow::OutputLine(const std::string &out) { void DebuggerWindow::OutputPrompt(const std::string& prompt) { } -void DebuggerWindow::Start(DebuggerShell* debugger) { +void DebuggerWindow::Start(DebuggerHost* debugger) { #ifndef CHROME_DEBUGGER_DISABLED DebuggerInputOutput::Start(debugger); #endif @@ -116,8 +116,6 @@ void DebuggerWindow::SetDebuggerBreak(bool brk) { if (debugger_break_ != brk) { debugger_break_ = brk; if (window_) { - if (view_) - view_->SetDebuggerBreak(brk); window_->UpdateWindowTitle(); if (brk) window_->Activate(); @@ -126,6 +124,17 @@ void DebuggerWindow::SetDebuggerBreak(bool brk) { #endif } +void DebuggerWindow::CallFunctionInPage(const std::wstring& name, + ListValue* argv) { + if (view_) { + DictionaryValue* body = new DictionaryValue; + body->Set(L"arguments", argv); + view_->SendEventToPage(name, body); + } else { + delete argv; + } +} + /////////////////////////////////////////////////////////////////// // views::WindowDelegate methods @@ -160,27 +169,3 @@ bool DebuggerWindow::CanResize() const { views::View* DebuggerWindow::GetContentsView() { return view_; } - -/////////////////////////////////////////////////////////////////// -// Overridden from views::TextField::Controller: - -void DebuggerWindow::ContentsChanged(views::TextField* sender, - const std::wstring& new_contents) { - // -} - -void DebuggerWindow::HandleKeystroke(views::TextField* sender, UINT message, - TCHAR key, UINT repeat_count, - UINT flags) { -#ifndef CHROME_DEBUGGER_DISABLED - if (key == VK_RETURN) { - std::wstring txt = sender->GetText(); - if (txt.length()) { - view_->Output(L"$ " + txt); - debugger_->ProcessCommand(txt); - sender->SetText(L""); - } - } -#endif -} - diff --git a/chrome/browser/debugger/debugger_window.h b/chrome/browser/debugger/debugger_window.h index 5ea1e57..4b980f3 100644 --- a/chrome/browser/debugger/debugger_window.h +++ b/chrome/browser/debugger/debugger_window.h @@ -6,16 +6,15 @@ #define CHROME_BROWSER_DEBUGGER_DEBUGGER_WINDOW_H__ #include "chrome/browser/debugger/debugger_io.h" -#include "chrome/views/text_field.h" #include "chrome/views/window.h" #include "chrome/views/window_delegate.h" class DebuggerView; +class ListValue; class TabContents; class DebuggerWindow : public DebuggerInputOutput, - public views::WindowDelegate, - public views::TextField::Controller { + public views::WindowDelegate { public: DebuggerWindow(); virtual ~DebuggerWindow(); @@ -33,22 +32,20 @@ class DebuggerWindow : public DebuggerInputOutput, virtual void Output(const std::string& out); virtual void OutputLine(const std::string& out); virtual void OutputPrompt(const std::string& prompt); - virtual void Start(DebuggerShell* debugger); + virtual void Start(DebuggerHost* debugger); virtual void SetDebuggerReady(bool ready); virtual void SetDebuggerBreak(bool brk); + // Note that this method will take ownership of argv. + virtual void CallFunctionInPage(const std::wstring& name, + ListValue* argv); + // views::WindowDelegate methods: virtual std::wstring GetWindowTitle() const; virtual void WindowClosing(); virtual bool CanResize() const; virtual views::View* GetContentsView(); - // Overridden from views::TextField::Controller: - virtual void ContentsChanged(views::TextField* sender, - const std::wstring& new_contents); - virtual void HandleKeystroke(views::TextField* sender, UINT message, - TCHAR key, UINT repeat_count, UINT flags); - private: views::Window* window_; DebuggerView* view_; diff --git a/chrome/browser/debugger/debugger_wrapper.cc b/chrome/browser/debugger/debugger_wrapper.cc index 9b29903..674b092 100644 --- a/chrome/browser/debugger/debugger_wrapper.cc +++ b/chrome/browser/debugger/debugger_wrapper.cc @@ -2,9 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "debugger_wrapper.h" -#include "debugger_shell.h" -#include "debugger_io_socket.h" +#include "chrome/browser/debugger/debugger_wrapper.h" +#include "chrome/browser/debugger/debugger_shell.h" +#include "chrome/browser/debugger/debugger_io_socket.h" +#include "chrome/browser/debugger/debugger_host.h" DebuggerWrapper::DebuggerWrapper(int port) { #ifndef CHROME_DEBUGGER_DISABLED @@ -19,11 +20,11 @@ DebuggerWrapper::DebuggerWrapper(int port) { DebuggerWrapper::~DebuggerWrapper() { } -void DebuggerWrapper::SetDebugger(DebuggerShell* debugger) { +void DebuggerWrapper::SetDebugger(DebuggerHost* debugger) { debugger_ = debugger; } -DebuggerShell* DebuggerWrapper::GetDebugger() { +DebuggerHost* DebuggerWrapper::GetDebugger() { return debugger_.get(); } diff --git a/chrome/browser/debugger/debugger_wrapper.h b/chrome/browser/debugger/debugger_wrapper.h index 2befe70..f1e68cc 100644 --- a/chrome/browser/debugger/debugger_wrapper.h +++ b/chrome/browser/debugger/debugger_wrapper.h @@ -23,7 +23,7 @@ #include "base/basictypes.h" #include "base/ref_counted.h" -class DebuggerShell; +class DebuggerHost; class DebuggerWrapper : public base::RefCountedThreadSafe<DebuggerWrapper> { public: @@ -31,8 +31,8 @@ class DebuggerWrapper : public base::RefCountedThreadSafe<DebuggerWrapper> { virtual ~DebuggerWrapper(); - void SetDebugger(DebuggerShell* debugger); - DebuggerShell* GetDebugger(); + void SetDebugger(DebuggerHost* debugger); + DebuggerHost* GetDebugger(); void DebugMessage(const std::wstring& msg); @@ -40,7 +40,7 @@ class DebuggerWrapper : public base::RefCountedThreadSafe<DebuggerWrapper> { void OnDebugDisconnect(); private: - scoped_refptr<DebuggerShell> debugger_; + scoped_refptr<DebuggerHost> debugger_; }; #endif // CHROME_BROWSER_DEBUGGER_DEBUGGER_INTERFACE_H_ diff --git a/webkit/build/port/port.vcproj b/webkit/build/port/port.vcproj index 8412ecf..df93fe1 100644 --- a/webkit/build/port/port.vcproj +++ b/webkit/build/port/port.vcproj @@ -224,6 +224,31 @@ > </File> <File + RelativePath="..\..\port\page\inspector\debugger.css" + > + </File> + <File + RelativePath="..\..\port\page\inspector\debugger.html" + > + </File> + <File + RelativePath="..\..\port\page\inspector\DebuggerConsole.js" + > + </File> + <File + RelativePath="..\..\port\page\inspector\DebuggerIPC.js" + > + </File> + <File + RelativePath="..\..\port\page\inspector\DebuggerPanel.js" + DeploymentContent="true" + > + </File> + <File + RelativePath="..\..\port\page\inspector\DebuggerShell.js" + > + </File> + <File RelativePath="..\..\..\third_party\WebKit\WebCore\inspector\front-end\ElementsPanel.js" > </File> diff --git a/webkit/port/page/inspector/DebuggerConsole.js b/webkit/port/page/inspector/DebuggerConsole.js new file mode 100644 index 0000000..0ba3b28 --- /dev/null +++ b/webkit/port/page/inspector/DebuggerConsole.js @@ -0,0 +1,173 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Helper functions and objects for the JS debugger UI. + * @see debugger.html + */ + +/** + * Document load listener. + */ +function onLoad() { + var debuggerConsole = new DebuggerConsole(); + DebuggerIPC.init(debuggerConsole); + DebugShell.initDebugShell(debuggerConsole); + debuggerConsole.focusOnCommandLine(); +} + +/** + * @constructor + */ +function DebuggerConsole() +{ + this._output = document.getElementById("output"); + + var input = document.getElementById("command-line-text"); + var self = this; + input.addEventListener( + 'keydown', + function(e) { + return self._onInputKeyDown(e); + }, + true); + input.addEventListener( + 'keypress', + function(e) { + return self._onInputKeyPress(e); + }, + true); + this._commandLineInput = input; + + // command object stores command-line history state. + this._command = { + history: [], + history_index: 0, + pending: null + }; +}; + +DebuggerConsole.prototype = { + /** + * Sets focus to command-line-text element. + */ + focusOnCommandLine: function() { + this._commandLineInput.focus(); + }, + + /** + * Called by chrome code when there's output to display. + * @param {string} txt + */ + appendText: function(txt) + { + this._output.appendChild(document.createTextNode(txt)); + this._output.appendChild(document.createElement('br')); + document.body.scrollTop = document.body.scrollHeight; + }, + + /** + * Called by chrome code to set the current state as to whether the debugger + * is stopped at a breakpoint or is running. + * @param {boolean} isBroken + */ + setDebuggerBreak: function(isBroken) + { + var out = this._output; + if (isBroken) { + out.style.color = "black"; + this.focusOnCommandLine(); + } else { + out.style.color = "gray"; + } + }, + + /** + * Execute a debugger command, add it to the command history and display it in + * the output window. + * @param {string} str + */ + executeCommand: function(str) { + this.appendText("$ " + str); + // Sends message to DebuggerContents.HandleCommand. + if (DebugShell.singleton) { + DebugShell.singleton.command(str); + } else { + this.appendText("FAILED to send the command as DebugShell is null"); + } + + this._command.history.push(str); + this._command.history_index = this._command.history.length; + this._command.pending = null; + }, + + + /** + * Display the previous history item in the given text field. + * @param {HTMLInputElement} field + */ + selectPreviousCommand_: function(field) { + var command = this._command; + if (command.history_index > 0) { + // Remember the current field value as a pending command if we're at the + // end (it's something the user typed in). + if (command.history_index == command.history.length) + command.pending = field.value; + command.history_index--; + field.value = command.history[command.history_index]; + field.select(); + } + }, + + /** + * Display the next history item in the given text field. + * @param {HTMLInputElement} field + */ + selectNextCommand_: function(field) { + var command = this._command; + if (command.history_index < command.history.length) { + command.history_index++; + if (command.history_index == command.history.length) { + field.value = command.pending || ""; + } else { + field.value = command.history[command.history_index]; + } + field.select(); + } + }, + + /** + * command-line-text's onkeydown handler. + * @param {KeyboardEvent} e + * @return {boolean} + */ + _onInputKeyDown: function (e) { + var field = e.target; + var key = e.keyCode; + if (key == 38) { // up arrow + this.selectPreviousCommand_(field); + return false; + } else if (key == 40) { // down arrow + this.selectNextCommand_(field); + return false; + } + return true; + }, + + /** + * command-line-text's onkeypress handler. + * @param {KeyboardEvent} e + * @return {boolean} + */ + _onInputKeyPress: function (e) { + var field = e.target; + var key = e.keyCode; + if (key == 13) { // enter + this.executeCommand(field.value); + field.value = ""; + return false; + } + return true; + } +}; diff --git a/webkit/port/page/inspector/DebuggerIPC.js b/webkit/port/page/inspector/DebuggerIPC.js new file mode 100644 index 0000000..15835fc --- /dev/null +++ b/webkit/port/page/inspector/DebuggerIPC.js @@ -0,0 +1,109 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Implementation of debugger inter-process communication. + * Debugger UI can send messages to the DebuggerHost object living in + * Chrome browser process. The messages are either processed by DebuggerHost + * itself or trigger IPC message(such as break, evaluate script etc) from + * browser process to the renderer where the v8 instance being debugged + * will process them. + */ + +var DebuggerIPC = {}; + +/** + * Set up default debugger UI. + * @param {DebuggerPanel|DebuggerConsole} debuggerUI + */ +DebuggerIPC.init = function(debuggerUI) { + DebuggerIPC.debuggerUI = debuggerUI; +} + +/** + * Sends JSON message to DebuggerHost. + * @param {Array.<Object>} nameAndArguments + */ +DebuggerIPC.sendMessage = function(nameAndArguments) { + //alert("DebuggerIPC.sendMessage " + nameAndArguments); + dprint("DebuggerIPC.callMethod([" + nameAndArguments + "])"); + // convert all arguments to strings + // TODO(yurys): extend chrome.send to accept array of any value + // objects not only strings + for (var i = 0; i < nameAndArguments.length; i++) { + if (typeof nameAndArguments[i] != "string") { + nameAndArguments[i] = "" + nameAndArguments[i]; + } + } + + chrome.send("DebuggerHostMessage", nameAndArguments); +}; + +/** + * Handles messages from DebuggerHost. + * @param {Object} msg An object representing the message. + */ +DebuggerIPC.onMessageReceived = function(msg) { + //alert("DebuggerIPC.onMessageReceived " + msg.event); + var ui = DebuggerIPC.debuggerUI; + dprint("onMessageReceived: " + (msg && msg.event)); + if (msg.type == "event") { + if (msg.event == "setDebuggerBreak") { + var val = msg.body.argument; + ui.setDebuggerBreak(val); + } else if (msg.event == "appendText") { + var text = msg.body.text; + ui.appendText(text); + } else if (msg.event == "focusOnCommandLine") { + dprint("focusOnCommandLine event received"); + // messages to DebugShell + } else if (msg.event == "initDebugShell") { + // DebugShell.initDebugShell(); + dprint(msg.event + " done"); + } else if (msg.event == "on_attach") { + if (DebugShell.singleton) { + var args = msg.body.arguments; + if (!args || args.length != 1) { + dprint(msg.event + " failed: invalid arguments"); + return; + } + var title = args[0]; + DebugShell.singleton.on_attach(title); + dprint(msg.event + " done"); + } else { + dprint(msg.event + " failed: DebugShell.singleton == null"); + } + } else if (msg.event == "on_disconnect") { + if (DebugShell.singleton) { + DebugShell.singleton.on_disconnect(); + dprint(msg.event + " done"); + } else { + dprint(msg.event + " failed: DebugShell.singleton == null"); + } + } else if (msg.event == "exit") { + if (DebugShell.singleton) { + DebugShell.singleton.exit(); + dprint(msg.event + " done"); + } else { + dprint(msg.event + " failed: DebugShell.singleton == null"); + } + } else if (msg.event == "response") { + if (DebugShell.singleton) { + var args = msg.body.arguments; + if (!args || args.length != 1) { + dprint(msg.event + " failed: invalid argument"); + return; + } + DebugShell.singleton.response(args[0]); + dprint(msg.event + " done"); + } else { + ui.appendText(msg.event + " failed: DebugShell.singleton == null"); + } + } else { + ui.appendText("Unknown event: " + msg.event); + } + } else { + ui.appendText("Unknown message type: " + msg.type); + } +}; diff --git a/webkit/port/page/inspector/DebuggerPanel.js b/webkit/port/page/inspector/DebuggerPanel.js new file mode 100644 index 0000000..bb059ac --- /dev/null +++ b/webkit/port/page/inspector/DebuggerPanel.js @@ -0,0 +1,201 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview WebInspector panel representing command line debugger. + */ + +/** + * Command line debugger panel. + * @constructor + * @extends {WebInspector.Panel} + */ +WebInspector.DebuggerPanel = function() +{ + WebInspector.Panel.call(this); + + this.contentElement = document.createElement("div"); + this.contentElement.id = "debugger-content"; + + var table = document.createElement("table"); + table.id = 'outer'; + var tr = document.createElement("tr"); + this._output = document.createElement("td"); + this._output.id = "output"; + this._output.valign = "bottom"; + this.appendText("Chrome JavaScript Debugger"); + this.appendText("Type 'help' for a list of commands."); + + tr.appendChild(this._output); + table.appendChild(tr); + this.contentElement.appendChild(table); + + + var commandLine = document.createElement("div"); + commandLine.id = "command-line"; + var input = document.createElement("input"); + input.id = "command-line-text"; + input.addEventListener('keydown', this._onInputKeyDown.bind(this), true); + input.addEventListener('keypress', this._onInputKeyPress.bind(this), true); + input.type = "text"; + this.commandLineInput_ = input; + + commandLine.appendChild(input); + this.contentElement.appendChild(commandLine); + + // command object stores command-line history state. + this._command = { + history: [], + history_index: 0, + pending: null + }; + + this.element.appendChild(this.contentElement); + p('DebuggerPanel created'); +}; + +WebInspector.DebuggerPanel.prototype = { + toolbarItemClass: "debugger", + + get toolbarItemLabel() + { + return "Debugger"; //WebInspector.UIString("Debugger"); + }, + + show: function() + { + WebInspector.Panel.prototype.show.call(this); + this.focusOnCommandLine(); + }, + + /** + * Sets focus to command-line-text element. + */ + focusOnCommandLine: function() + { + if (this.visible) { + this.commandLineInput_.focus(); + } + }, + + /** + * Called by chrome code when there's output to display. + * @param {string} txt + */ + appendText: function(txt) + { + this._output.appendChild(document.createTextNode(txt)); + this._output.appendChild(document.createElement('br')); + }, + + /** + * Called by chrome code to set the current state as to whether the debugger + * is stopped at a breakpoint or is running. + * @param {boolean} isBroken + */ + setDebuggerBreak: function(isBroken) + { + var out = this._output; + if (isBroken) { + out.style.color = "black"; + this.focusOnCommandLine(); + } else { + out.style.color = "gray"; + } + }, + + /** + * Execute a debugger command, add it to the command history and display + * it in the output window. + * @param {string} str + */ + executeCommand: function(str) + { + this.appendText("$ " + str); + // Sends message to DebuggerContents.HandleCommand. + if (DebugShell.singleton) { + DebugShell.singleton.command(str); + } else { + this.appendText("FAILED to send the command as DebugShell is null"); + } + + this._command.history.push(str); + this._command.history_index = this._command.history.length; + this._command.pending = null; + }, + + /** + * Display the previous history item in the given text field. + * @param {HTMLInputElement} field + */ + _selectPreviousCommand: function(field) + { + var command = this._command; + if (command.history_index > 0) { + // Remember the current field value as a pending command if we're at the + // end (it's something the user typed in). + if (command.history_index == command.history.length) + command.pending = field.value; + command.history_index--; + field.value = command.history[command.history_index]; + field.select(); + } + }, + + /** + * Display the next history item in the given text field. + * @param {HTMLInputElement} field + */ + _selectNextCommand: function(field) + { + var command = this._command; + if (command.history_index < command.history.length) { + command.history_index++; + if (command.history_index == command.history.length) { + field.value = command.pending || ""; + } else { + field.value = command.history[command.history_index]; + } + field.select(); + } + }, + + /** + * command-line-text's onkeydown handler. + * @param {KeyboardEvent} e + * @return {boolean} + */ + _onInputKeyDown: function (e) + { + var field = e.target; + var key = e.keyCode; + if (key == 38) { // up arrow + this._selectPreviousCommand(field); + return false; + } else if (key == 40) { // down arrow + this._selectNextCommand(field); + return false; + } + return true; + }, + + /** + * command-line-text's onkeypress handler. + * @param {KeyboardEvent} e + * @return {boolean} + */ + _onInputKeyPress: function (e) + { + var field = e.target; + var key = e.keyCode; + if (key == 13) { // enter + this.executeCommand(field.value); + field.value = ""; + return false; + } + return true; + } +}; + +WebInspector.DebuggerPanel.prototype.__proto__ = WebInspector.Panel.prototype; diff --git a/webkit/port/page/inspector/DebuggerShell.js b/webkit/port/page/inspector/DebuggerShell.js new file mode 100644 index 0000000..804bb9a --- /dev/null +++ b/webkit/port/page/inspector/DebuggerShell.js @@ -0,0 +1,1327 @@ +// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview Shell objects and global helper functions for Chrome + * automation shell / debugger. This file is loaded into the global namespace + * of the interactive shell, so users can simply call global functions + * directly. + */ + +// TODO(erikkay): look into how this can be split up into multiple files +// It's currently loaded explicitly by Chrome, so maybe I need an "include" +// or "source" builtin to allow a core source file to reference multiple +// sub-files. + +/** + * Sequence number of the DebugCommand. + */ +DebugCommand.next_seq_ = 0; + +/** + * Command messages to be sent to the debugger. + * @constructor + */ +function DebugCommand(str) { + this.command = undefined; + // first, strip off of the leading word as the command + var argv = str.split(' '); + this.user_command = argv.shift(); + // the rest of the string is argv to the command + str = argv.join(' '); + if (DebugCommand.aliases[this.user_command]) + this.user_command = DebugCommand.aliases[this.user_command]; + if (this.parseArgs_(str) == 1) + this.type = "request"; + if (this.command == undefined) + this.command = this.user_command; +}; + +// Mapping of some control characters to avoid the \uXXXX syntax for most +// commonly used control cahracters. +const ctrlCharMap_ = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' +}; + +// Regular expression matching ", \ and control characters (0x00 - 0x1F) +// globally. +const ctrlCharMatch_ = /["\\\\\x00-\x1F]/g; + +/** + * Convert a String to its JSON representation. + * @param {String} value - String to be converted + * @return {String} JSON formatted String + */ +DebugCommand.stringToJSON = function(value) { + // Check for" , \ and control characters (0x00 - 0x1F). + if (ctrlCharMatch_.test(value)) { + // Replace ", \ and control characters (0x00 - 0x1F). + return '"' + value.replace(ctrlCharMatch_, function (char) { + // Use charmap if possible. + var mapped = ctrlCharMap_[char]; + if (mapped) return mapped; + mapped = char.charCodeAt(); + // Convert control character to unicode escape sequence. + var dig1 = (Math.floor(mapped / 16)); + var dig2 = (mapped % 16) + return '\\u00' + dig1.toString(16) + dig2.toString(16); + }) + + '"'; + } + + // Simple string with no special characters. + return '"' + value + '"'; +}; + +/** + * @return {bool} True if x is an integer. + */ +DebugCommand.isInt = function(x) { + var y = parseInt(x); + if (isNaN(y)) + return false; + return x == y && x.toString() == y.toString(); +}; + +/** + * @return {float} log base 10 of num + */ +DebugCommand.log10 = function(num) { + return Math.log(num)/Math.log(10); +}; + +/** + * Take an object and encode it (non-recursively) as a JSON dict. + * @param {Object} obj - object to encode + */ +DebugCommand.toJSON = function(obj) { + // TODO(erikkay): use a real JSON library + var json = '{'; + for (var key in obj) { + if (json.length > 1) + json += ","; + var val = obj[key]; + if (!DebugCommand.isInt(val)) { + val = DebugCommand.stringToJSON(val.toString()); + } + json += '"' + key + '":' + val; + } + json += '}'; + return json; +}; + +/** + * Encode the DebugCommand object into the V8 debugger JSON protocol format. + */ +DebugCommand.prototype.toJSONProtocol = function() { + // TODO(erikkay): use a real JSON library + var json = '{'; + json += '"seq":"' + this.seq; + json += '","type":"' + this.type; + json += '","command":"' + this.command + '"'; + if (this.arguments) { + json += ',"arguments":' + DebugCommand.toJSON(this.arguments); + } + json += '}' + return json; +} + +/** + * Encode the contents of this message and send it to the debugger. + * @param {Object} tab - tab being debugged. This is an internal + * Chrome object. + */ +DebugCommand.prototype.sendToDebugger = function(tab) { + this.seq = DebugCommand.next_seq_++; + str = this.toJSONProtocol(); + dprint("sending: " + str); + tab.sendToDebugger(str); +}; + +DebugCommand.trim = function(str) { + return str.replace(/^\s*/, '').replace(/\s*$/, ''); +}; + +/** + * Strip off a trailing parameter after a ':'. As the identifier for the + * source can contain ':' characters (e.g. 'http://www....) something after + * a ':' is only considered a parameter if it is numeric. + * @return {Array} two element array, the trimmed string and the parameter, + * or -1 if no parameter + */ +DebugCommand.stripTrailingParameter = function(str, opt_separator) { + var sep = opt_separator || ':'; + var index = str.lastIndexOf(sep); + // If a separator character if found strip if numeric. + if (index != -1) { + var value = parseInt(str.substring(index + 1, str.length), 10); + if (isNaN(value) || value < 0) { + return [str, -1]; + } + str = str.substring(0, index); + return [str, value]; + } + return [str, -1]; +}; + +/** + * Format source and location strings based on source location input data. + * @param {Object} script - script information object + * @param {String} source - source code for the current location + * @param {int} line - line number (0-based) + * @param {String} func - function name + * @return {array} [location(string), source line(string), line number(int)] + */ +DebugCommand.getSourceLocation = function(script, source, line, func) { + // source line is 0-based, we present as 1-based + line++; + + // TODO(erikkay): take column into account as well + if (source) + source = "" + line + ": " + source; + var location = ''; + if (func) { + location = func + ", "; + } + location += script ? script.name : '[no source]'; + return [location, source, line]; +}; + +/** + * Aliases for debugger commands. + */ +DebugCommand.aliases = { + 'b': 'break', + 'bi': 'break_info', + 'br': 'break', + 'bt': 'backtrace', + 'c': 'continue', + 'f': 'frame', + 'h': 'help', + '?': 'help', + 'ls': 'source', + 'n': 'next', + 'p': 'print', + 's': 'step', + 'so': 'stepout', +}; + +/** + * Parses arguments to "args" and "locals" command, and initializes + * the underlying DebugCommand (which is a frame request). + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseArgsAndLocals_ = function(str) { + this.command = "frame"; + return str.length ? -1 : 1; +}; + +/** + * Parses arguments to "break_info" command, and executes it. + * "break_info" has an optional argument, which is the breakpoint + * identifier. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 0 for success + */ +DebugCommand.prototype.parseBreakInfo_ = function(str) { + this.type = "shell"; + + // Array of breakpoints to be printed by this command + // (default to all breakpoints) + var breakpointsToPrint = shell_.breakpoints; + + if (str.length > 0) { + // User specified an invalid breakpoint (not a number) + if (!str.match(/^\s*\d+\s*$/)) + return -1; // invalid usage + + // Check that the specified breakpoint identifier exists + var id = parseInt(str); + var info = shell_.breakpoints[id]; + if (!info) { + print("Error: Invalid breakpoint"); + return 0; // success (of sorts) + } + breakpointsToPrint = [info]; + } else { + // breakpointsToPrint.length isn't accurate, because of + // deletions + var num_breakpoints = 0; + for (var i in breakpointsToPrint) num_breakpoints++; + + print("Num breakpoints: " + num_breakpoints); + } + + DebugShell.printBreakpoints_(breakpointsToPrint); + + return 0; // success +} + +/** + * Parses arguments to "step" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseStep_ = function(str, opt_stepaction) { + this.command = "continue"; + action = opt_stepaction || "in"; + this.arguments = {"stepaction" : action} + if (str.length) { + count = parseInt(str); + if (count > 0) { + this.arguments["stepcount"] = count; + } else { + return -1; + } + } + return 1; +}; + +/** + * Parses arguments to "step" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseStepOut_ = function(str) { + return this.parseStep_(str, "out"); +}; + +/** + * Parses arguments to "next" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseNext_ = function(str) { + return this.parseStep_(str, "next"); +}; + +/** + * Parse the arguments to "print" command. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return 1 - always succeeds + */ +DebugCommand.prototype.parsePrint_ = function(str) { + this.command = "evaluate"; + this.arguments = { "expression" : str }; + // If the page is in the running state, then we force the expression to + // evaluate in the global context to avoid evaluating in a random context. + if (shell_.running) + this.arguments["global"] = true; + return 1; +}; + +/** + * Handle the response to a "print" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responsePrint_ = function(msg) { + body = msg["body"]; + if (body['text'] != undefined) { + print(body['text']); + } else { + // TODO(erikkay): is "text" ever not set? + print("can't print response"); + } +}; + +/** + * Parses arguments to "break" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success, 0 for handled internally + */ +DebugCommand.prototype.parseBreak_ = function(str) { + function stripTrailingParameter() { + var ret = DebugCommand.stripTrailingParameter(str, ':'); + str = ret[0]; + return ret[1]; + } + + if (str.length == 0) { + this.command = "break"; + return 1; + } else { + var parts = str.split(/\s+/); + var condition = null; + if (parts.length > 1) { + str = parts.shift(); + condition = parts.join(" "); + } + + this.command = "setbreakpoint"; + + // Locate ...[:line[:column]] if present. + var line = -1; + var column = -1; + line = stripTrailingParameter(); + if (line != -1) { + line -= 1; + var l = stripTrailingParameter(); + if (l != -1) { + column = line; + line = l - 1; + } + } + + if (line == -1 && column == -1) { + this.arguments = { 'type' : 'function', + 'target' : str }; + } else { + var script = shell_.matchScript(str, line); + if (script) { + this.arguments = { 'type' : 'script', + 'target' : script.name }; + } else { + this.arguments = { 'type' : 'function', + 'target' : str }; + } + this.arguments.line = line; + if (column != -1) + this.arguments.position = column; + } + if (condition) + this.arguments.condition = condition; + } + return 1; +}; + +/** + * Handle the response to a "break" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseBreak_ = function(msg) { + var info = new BreakpointInfo( + parseInt(msg.body.breakpoint), + msg.command.arguments.type, + msg.command.arguments.target, + msg.command.arguments.line, + msg.command.arguments.position, + msg.command.arguments.condition); + shell_.addedBreakpoint(info); +}; + +/** + * Parses arguments to "backtrace" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ + DebugCommand.prototype.parseBacktrace_ = function(str) { + if (str.length > 0) { + var parts = str.split(/\s+/); + var non_empty_parts = parts.filter(function(s) { return s.length > 0; }); + // We need exactly two arguments. + if (non_empty_parts.length != 2) { + return -1; + } + var from = parseInt(non_empty_parts[0], 10); + var to = parseInt(non_empty_parts[1], 10); + // The two arguments have to be integers. + if (from != non_empty_parts[0] || to != non_empty_parts[1]) { + return -1; + } + this.arguments = { 'fromFrame': from, 'toFrame': to + 1 }; + } else { + // Default to fetching the first 10 frames. + this.arguments = { 'fromFrame': 0, 'toFrame': 10 }; + } + return 1; +}; + +/** + * Handle the response to a "backtrace" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseBacktrace_ = function(msg) { + body = msg["body"]; + if (body && body.totalFrames) { + print('Frames #' + body.fromFrame + ' to #' + (body.toFrame - 1) + + ' of ' + body.totalFrames + ":"); + for (var i = 0; i < body.frames.length; i++) { + print(body.frames[i].text); + } + } else { + print("unimplemented (sorry)"); + } +}; + + +/** + * Parses arguments to "clear" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseClearCommand_ = function(str) { + this.command = "clearbreakpoint"; + if (str.length > 0) { + var i = parseInt(str, 10); + if (i != str) { + return -1; + } + this.arguments = { 'breakpoint': i }; + } + return 1; +} + +/** + * Handle the response to a "clear" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseClear_ = function(msg) { + shell_.clearedBreakpoint(parseInt(msg.command.arguments.breakpoint)); +} + + +/** + * Parses arguments to "continue" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseContinueCommand_ = function(str) { + this.command = "continue"; + if (str.length > 0) { + return -1; + } + return 1; +} + +/** + * Parses arguments to "frame" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseFrame_ = function(str) { + if (str.length > 0) { + var i = parseInt(str, 10); + if (i != str) { + return -1; + } + this.arguments = { 'number': i }; + } + return 1; +}; + +/** + * Handle the response to a "frame" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseFrame_ = function(msg) { + body = msg.body; + loc = DebugCommand.getSourceLocation(body.func.script, + body.sourceLineText, body.line, body.func.name); + print("#" + (body.index <= 9 ? '0' : '') + body.index + " " + loc[0]); + print(loc[1]); + shell_.current_frame = body.index; + shell_.current_line = loc[2]; + shell_.current_script = body.func.script; +}; + +/** + * Handle the response to a "args" command and display output to user. + * @param {Object} msg - the V8 debugger response object (for "frame" command) + */ +DebugCommand.responseArgs_ = function(msg) { + DebugCommand.printVariables_(msg.body.arguments); +} + +/** + * Handle the response to a "locals" command and display output to user. + * @param {Object} msg - the V8 debugger response object (for "frame" command) + */ +DebugCommand.responseLocals_ = function(msg) { + DebugCommand.printVariables_(msg.body.locals); +} + +DebugCommand.printVariables_ = function(variables) { + for (var i = 0; i < variables.length; i++) { + print(variables[i].name + " = " + + DebugCommand.toPreviewString_(variables[i].value)); + } +} + +DebugCommand.toPreviewString_ = function(value) { + // TODO(ericroman): pretty print arrays and objects, recursively. + // TODO(ericroman): truncate length of preview if too long? + if (value.type == "string") { + // Wrap the string in quote marks and JS-escape + return DebugCommand.stringToJSON(value.text); + } + return value.text; +} + +/** + * Parses arguments to "scripts" command. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseScripts_ = function(str) { + return 1 +}; + +/** + * Handle the response to a "scripts" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseScripts_ = function(msg) { + scripts = msg.body; + shell_.scripts = []; + for (var i in scripts) { + var script = scripts[i]; + + // Add this script to the internal list of scripts. + shell_.scripts.push(script); + + // Print result if this response was the result of a user command. + if (msg.command.from_user) { + var name = script.name; + if (name) { + if (script.lineOffset > 0) { + print(name + " (lines " + script.lineOffset + "-" + + (script.lineOffset + script.lineCount - 1) + ")"); + } else { + print(name + " (lines " + script.lineCount + ")"); + } + } else { + // For unnamed scripts (typically eval) display some source. + var sourceStart = script.sourceStart; + if (sourceStart.length > 40) + sourceStart = sourceStart.substring(0, 37) + '...'; + print("[unnamed] (source:\"" + sourceStart + "\")"); + } + } + } +}; + +/** + * Parses arguments to "source" command. + * @see DebugCommand.commands + * @param {string} str - The arguments to be parsed. + * @return -1 for usage error, 1 for success + */ +DebugCommand.prototype.parseSource_ = function(str) { + this.arguments = {}; + if (this.current_frame > 0) + this.arguments.frame = this.current_frame; + if (str.length) { + var args = str.split(" "); + if (args.length == 1) { + // with 1 argument n, we print 10 lines starting at n + var num = parseInt(args[0]); + if (num > 0) { + this.arguments.fromLine = num - 1; + this.arguments.toLine = this.arguments.fromLine + 10; + } else { + return -1; + } + } else if (args.length == 2) { + // with 2 arguments x and y, we print from line x to line x + y + var from = parseInt(args[0]); + var len = parseInt(args[1]); + if (from > 0 && len > 0) { + this.arguments.fromLine = from - 1; + this.arguments.toLine = this.arguments.fromLine + len; + } else { + return -1; + } + } else { + return -1; + } + if (this.arguments.fromLine < 0) + return -1; + if (this.arguments.toLine <= this.arguments.fromLine) + return -1; + } else if (shell_.current_line > 0) { + // with no arguments, we print 11 lines with the current line as the center + this.arguments.fromLine = + Math.max(0, shell_.current_line - 6); + this.arguments.toLine = this.arguments.fromLine + 11; + } + return 1; +}; + +/** + * Handle the response to a "source" command and display output to user. + * @param {Object} msg - the V8 debugger response object + */ +DebugCommand.responseSource_ = function(msg) { + var body = msg.body; + var from_line = parseInt(body.fromLine) + 1; + var source = body.source; + var lines = source.split('\n'); + var maxdigits = 1 + Math.floor(DebugCommand.log10(from_line + lines.length)) + for (var num in lines) { + // there's an extra newline at the end + if (num >= (lines.length - 1) && lines[num].length == 0) + break; + spacer = maxdigits - (1 + Math.floor(DebugCommand.log10(from_line))) + var line = ""; + if (from_line == shell_.current_line) { + for (var i = 0; i < (maxdigits + 2); i++) + line += ">"; + } else { + for (var i = 0; i < spacer; i++) + line += " "; + line += from_line + ": "; + } + line += lines[num]; + print(line); + from_line++; + } +}; + +/** + * Parses arguments to "help" command. See DebugCommand.commands below + * for syntax details. + * @see DebugCommand.commands + * @param {string} str The arguments to be parsed. + * @return 0 for handled internally + */ +DebugCommand.parseHelp_ = function(str) { + DebugCommand.help(str); + return 0; +}; + +/** + * Takes argument and evaluates it in the context of the shell to allow commands + * to be escaped to the outer shell. Used primarily for development purposes. + * @see DebugCommand.commands + * @param {string} str The expression to be evaluated + * @return 0 for handled internally + */ +DebugCommand.parseShell_ = function(str) { + print(eval(str)); + return 0; +} + +DebugCommand.parseShellDebug_ = function(str) { + shell_.debug = !shell_.debug; + if (shell_.debug) { + print("shell debugging enabled"); + } else { + print("shell debugging disabled"); + } + return 0; +} + +/** + * Parses a user-entered command string. + * @param {string} str The arguments to be parsed. + */ +DebugCommand.prototype.parseArgs_ = function(str) { + if (str.length) + str = DebugCommand.trim(str); + var cmd = DebugCommand.commands[this.user_command]; + if (cmd) { + var parse = cmd['parse']; + if (parse == undefined) { + print('>>>can\'t find parse func for ' + this.user_command); + this.type = "error"; + } else { + var ret = parse.call(this, str); + if (ret > 0) { + // Command gererated a debugger request. + this.type = "request"; + } else if (ret == 0) { + // Command handeled internally. + this.type = "handled"; + } else if (ret < 0) { + // Command error. + this.type = "handled"; + DebugCommand.help(this.user_command); + } + } + } else { + this.type = "handled"; + print('unknown command: ' + this.user_command); + DebugCommand.help(); + } +}; + +/** + * Displays command help or all help. + * @param {string} opt_str Which command to print help for. + */ +DebugCommand.help = function(opt_str) { + if (opt_str) { + var cmd = DebugCommand.commands[opt_str]; + var usage = cmd.usage; + print('usage: ' + usage); + // Print additional details for the command. + if (cmd.help) { + print(cmd.help); + } + } else { + if (shell_.running) { + print('Status: page is running'); + } else { + print('Status: page is paused'); + } + print('Available commands:'); + for (var key in DebugCommand.commands) { + var cmd = DebugCommand.commands[key]; + if (!cmd['hidden'] && (!shell_.running || cmd['while_running'])) { + var usage = cmd.usage; + print(' ' + usage); + } + } + } +}; + +/** + * Valid commands, their argument parser and their associated usage text. + */ +DebugCommand.commands = { + 'args': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, + 'usage': 'args', + 'help': 'summarize the arguments to the current function.', + 'response': DebugCommand.responseArgs_ }, + 'break': { 'parse': DebugCommand.prototype.parseBreak_, + 'response': DebugCommand.responseBreak_, + 'usage': 'break [location] <condition>', + 'help': 'location is one of <function> | <script:function> | <script:line> | <script:line:pos>', + 'while_running': true }, + 'break_info': { 'parse': DebugCommand.prototype.parseBreakInfo_, + 'usage': 'break_info [breakpoint #]', + 'help': 'list the current breakpoints, or the details on a single one', + 'while_running': true }, + 'backtrace': { 'parse': DebugCommand.prototype.parseBacktrace_, + 'response': DebugCommand.responseBacktrace_, + 'usage': 'backtrace [from frame #] [to frame #]' }, + 'clear': { 'parse': DebugCommand.prototype.parseClearCommand_, + 'response': DebugCommand.responseClear_, + 'usage': 'clear <breakpoint #>', + 'while_running': true }, + 'continue': { 'parse': DebugCommand.prototype.parseContinueCommand_, + 'usage': 'continue' }, + 'frame': { 'parse': DebugCommand.prototype.parseFrame_, + 'response': DebugCommand.responseFrame_, + 'usage': 'frame <frame #>' }, + 'help': { 'parse': DebugCommand.parseHelp_, + 'usage': 'help [command]', + 'while_running': true }, + 'locals': { 'parse': DebugCommand.prototype.parseArgsAndLocals_, + 'usage': 'locals', + 'help': 'summarize the local variables for current frame', + 'response': DebugCommand.responseLocals_ }, + 'next': { 'parse': DebugCommand.prototype.parseNext_, + 'usage': 'next' } , + 'print': { 'parse': DebugCommand.prototype.parsePrint_, + 'response': DebugCommand.responsePrint_, + 'usage': 'print <expression>', + 'while_running': true }, + 'scripts': { 'parse': DebugCommand.prototype.parseScripts_, + 'response': DebugCommand.responseScripts_, + 'usage': 'scripts', + 'while_running': true }, + 'source': { 'parse': DebugCommand.prototype.parseSource_, + 'response': DebugCommand.responseSource_, + 'usage': 'source [from line] | [<from line> <num lines>]' }, + 'step': { 'parse': DebugCommand.prototype.parseStep_, + 'usage': 'step' }, + 'stepout': { 'parse': DebugCommand.prototype.parseStepOut_, + 'usage': 'stepout' }, + // local eval for debugging - remove this later + 'shell': { 'parse': DebugCommand.parseShell_, + 'usage': 'shell <expression>', + 'while_running': true, + 'hidden': true }, + 'shelldebug': { 'parse': DebugCommand.parseShellDebug_, + 'usage': 'shelldebug', + 'while_running': true, + 'hidden': true }, +}; + + +/** + * Debug shell using the new JSON protocol + * @param {Object} tab - which tab is to be debugged. This is an internal + * Chrome object. + * @constructor + */ +function DebugShell(tab) { + this.tab = tab; + this.tab.attach(); + this.ready = true; + this.running = true; + this.current_command = undefined; + this.pending_commands = []; + // The auto continue flag is used to indicate whether the JavaScript execution + // should automatically continue after a break event and the processing of + // pending commands. This is used to make it possible for the user to issue + // commands, e.g. setting break points, without making an explicit break. In + // this case the debugger will silently issue a forced break issue the command + // and silently continue afterwards. + this.auto_continue = false; + this.debug = false; + this.last_msg = undefined; + this.current_line = -1; + this.current_pos = -1; + this.current_frame = 0; + this.current_script = undefined; + this.scripts = []; + + // Mapping of breakpoints id --> info. + // Must use numeric keys. + this.breakpoints = []; +}; + +DebugShell.prototype.set_ready = function(ready) { + if (ready != this.ready) { + this.ready = ready; + ChromeNode.setDebuggerReady(this.ready); + } +}; + +DebugShell.prototype.set_running = function(running) { + if (running != this.running) { + this.running = running; + ChromeNode.setDebuggerBreak(!this.running); + } +}; + +/** + * Execute a constructed DebugCommand object if possible, otherwise pend. + * @param cmd {DebugCommand} - command to execute + */ +DebugShell.prototype.process_command = function(cmd) { + dprint("Running: " + (this.running ? "yes" : "no")); + + // The "break" commands needs to be handled seperatly + if (cmd.command == "break") { + if (this.running) { + // Schedule a break. + print("Stopping JavaScript execution..."); + this.tab.debugBreak(false); + } else { + print("JavaScript execution already stopped."); + } + return; + } + + // If page is running an break needs to be issued. + if (this.running) { + // Some requests are not valid when the page is running. + var cmd_info = DebugCommand.commands[cmd.user_command]; + if (!cmd_info['while_running']) { + print(cmd.user_command + " can only be run while paused"); + return; + } + + // Add the command as pending before scheduling a break. + this.pending_commands.push(cmd); + dprint("pending command: " + cmd.toJSONProtocol()); + + // Schedule a forced break and enable auto continue. + this.tab.debugBreak(true); + this.auto_continue = true; + this.set_ready(false); + return; + } + + // If waiting for a response add command as pending otherwise send the + // command. + if (this.current_command) { + this.pending_commands.push(cmd); + dprint("pending command: " + cmd.toJSONProtocol()); + } else { + this.current_command = cmd; + cmd.sendToDebugger(this.tab); + this.set_ready(false); + } +}; + +/** + * Handle a break event from the debugger. + * @param msg {Object} - event protocol message to handle + */ +DebugShell.prototype.event_break = function(msg) { + this.current_frame = 0; + this.set_running(false); + if (msg.body) { + var body = msg.body; + this.current_script = body.script; + var loc = DebugCommand.getSourceLocation(body.script, + body.sourceLineText, body.sourceLine, body.invocationText); + var location = loc[0]; + var source = loc[1]; + this.current_line = loc[2]; + if (msg.body.breakpoints) { + // Always disable auto continue if a real break point is hit. + this.auto_continue = false; + var breakpoints = msg.body.breakpoints; + print("paused at breakpoint " + breakpoints.join(",") + ": " + + location); + for (var i = 0; i < breakpoints.length; i++) + this.didHitBreakpoint(parseInt(breakpoints[i])); + } else if (body.scriptData == "") { + print("paused"); + } else { + // step, stepout, next, "break" and a "debugger" line in the code + // are all treated the same (they're not really distinguishable anyway) + if (location != this.last_break_location) { + // We only print the location (function + script) when it changes, + // so as we step, you only see the source line when you transition + // to a new script and/or function. Also if auto continue is enables + // don't print the break location. + if (!this.auto_continue) + print(location); + } + } + // Print th current source line unless auto continue is enabled. + if (source && !this.auto_continue) + print(source); + this.last_break_location = location; + } + if (!this.auto_continue) + this.set_ready(true); +}; + +/** + * Handle an exception event from the debugger. + * @param msg {Object} - event protocol message to handle + */ +DebugShell.prototype.event_exception = function(msg) { + this.set_running(false); + this.set_ready(true); + if (msg.body) { + if (msg.body["uncaught"]) { + print("uncaught exception " + msg.body["exception"].text); + } else { + print("paused at exception " + msg.body["exception"].text); + } + } +}; + +DebugShell.prototype.matchScript = function(script_match, line) { + var script = null; + // In the v8 debugger, all scripts have a name, line offset and line count + // Script names are usually URLs which are a pain to have to type again and + // again, so we match the tail end of the script name. This makes it easy + // to type break foo.js:23 rather than + // http://www.foo.com/bar/baz/quux/test/foo.js:23. In addition to the tail + // of the name we also look at the lines the script cover. If there are + // several scripts with the same tail including the requested line we match + // the first one encountered. + // TODO(sgjesse) Find how to handle several matching scripts. + var candidate_scripts = []; + for (var i in this.scripts) { + if (this.scripts[i].name && + this.scripts[i].name.indexOf(script_match) >= 0) { + candidate_scripts.push(this.scripts[i]); + } + } + for (var i in candidate_scripts) { + var s = candidate_scripts[i]; + var from = s.lineOffset; + var to = from + s.lineCount; + if (from <= line && line < to) { + script = s; + break; + } + } + if (script) + return script; + else + return null; +} + +// The Chrome Subshell interface requires: +// prompt(), command(), response(), exit() and on_disconnect() + +/** + * Called by Chrome Shell to get a prompt string to display. + */ +DebugShell.prototype.prompt = function() { + if (this.current_command) + return ''; + if (!this.running) + return 'v8(paused)> '; + else + return 'v8(running)> '; +}; + +/** + * Called by Chrome Shell when command input has been received from the user. + */ +DebugShell.prototype.command = function(str) { + if (this.tab) { + str = DebugCommand.trim(str); + if (str.length) { + var cmd = new DebugCommand(str); + cmd.from_user = true; + if (cmd.type == "request") + this.process_command(cmd); + } + } else { + print(">>not connected to a tab"); + } +}; + +/** + * Called when a response to a previous command has been received. + * @param {Object} msg Message object. + */ +DebugShell.prototype.response = function(msg) { + dprint("received: " + (msg && msg.type)); + this.last_msg = msg; + if (msg.type == "event") { + ev = msg["event"] + if (ev == "break") { + this.event_break(msg); + } else if (ev == "exception") { + this.event_exception(msg); + } + } else if (msg.type == "response") { + if (msg.request_seq != undefined) { + if (!this.current_command || this.current_command.seq != msg.request_seq){ + throw("received response to unknown command " + DebugCommand.toJSON(msg)); + } + } else { + // TODO(erikkay): should we reject these when they happen? + print(">>no request_seq in response " + DebugCommand.toJSON(msg)); + } + var cmd = DebugCommand.commands[this.current_command.user_command] + msg.command = this.current_command; + this.current_command = null + if (msg.running != undefined) { + this.set_running(msg.running); + } + if (!msg['success']) { + print(msg['message']); + } else { + var response = cmd['response']; + if (response != undefined) { + response.call(this, msg); + } + } + this.set_ready(true); + } + + // Process next pending command if any. + if (this.pending_commands.length) { + this.process_command(this.pending_commands.shift()); + } else if (this.auto_continue) { + // If no more pending commands and auto continue is active issue a continue command. + this.auto_continue = false; + this.process_command(new DebugCommand("continue")); + } +}; + +/** + * Called when a breakpoint has been set. + * @param {BreakpointInfo} info - details of breakpoint set. + */ +DebugShell.prototype.addedBreakpoint = function(info) { + print("set breakpoint #" + info.id); + this.breakpoints[info.id] = info; +} + +/** + * Called when a breakpoint has been cleared. + * @param {int} id - the breakpoint number that was cleared. + */ +DebugShell.prototype.clearedBreakpoint = function(id) { + assertIsNumberType(id, "clearedBreakpoint called with invalid id"); + + print("cleared breakpoint #" + id); + delete this.breakpoints[id]; +} + +/** + * Called when a breakpoint has been reached. + * @param {int} id - the breakpoint number that was hit. + */ +DebugShell.prototype.didHitBreakpoint = function(id) { + assertIsNumberType(id, "didHitBreakpoint called with invalid id"); + + var info = this.breakpoints[id]; + if (!info) + throw "Could not find breakpoint #" + id; + + info.hit_count ++; +} + +/** + * Print a summary of the specified breakpoints. + * + * @param {Array<BreakpointInfo>} breakpointsToPrint - List of breakpoints. The + * index is unused (id is determined from the info). + */ +DebugShell.printBreakpoints_ = function(breakpoints) { + // TODO(ericroman): this would look much nicer if we could output as an HTML + // table. I tried outputting as formatted text table, but this looks aweful + // once it triggers wrapping (which is very likely if the target is a script) + + // Output as a comma separated list of key=value + for (var i in breakpoints) { + var b = breakpoints[i]; + var props = ["id", "hit_count", "type", "target", "line", "position", + "condition"]; + var propertyList = []; + for (var i = 0; i < props.length; i++) { + var prop = props[i]; + var val = b[prop]; + if (val != undefined) + propertyList.push(prop + "=" + val); + } + print(propertyList.join(", ")); + } +} + +/** + * Called by Chrome Shell when the outer shell is detaching from debugging + * this tab. + */ +DebugShell.prototype.exit = function() { + if (this.tab) { + this.tab.detach(); + this.tab = null; + } +}; + +/** + * Called by the Chrome Shell when the tab that the shell is debugging + * have attached. + */ +DebugShell.prototype.on_attach = function(title) { + if (!title) + title = "Untitled"; + print('attached to ' + title); + // on attach, we update our current script list + var cmd = new DebugCommand("scripts"); + cmd.from_user = false; + this.process_command(cmd); +}; + + +/** + * Called by the Chrome Shell when the tab that the shell is debugging + * went away. + */ +DebugShell.prototype.on_disconnect = function() { + print(">>lost connection to tab"); + this.tab = null; +}; + + +/** + * Structure that holds the details about a breakpoint. + * @constructor + * + * @param {int} id - breakpoint number + * @param {string} type - "script" or "function" + * @param {string} target - either a function name, or script url + * @param {int} line - line number in the script, or undefined + * @param {int} position - column in the script, or undefined + * @param {string} condition - boolean expression, or undefined + */ +function BreakpointInfo(id, type, target, line, position, condition) { + this.id = id; + this.type = type; + this.target = target; + + if (line != undefined) + this.line = line; + if (position != undefined) + this.position = position; + if (condition != undefined) + this.condition = condition; + + this.hit_count = 0; + + // Check that the id is numeric, otherwise will run into problems later + assertIsNumberType(this.id, "id is not a number"); +} + +var shell_ = null; +DebugShell.initDebugShell = function(debuggerUI) { + if (!DebugShell.singleton) { + DebugShell.ui = debuggerUI; + DebugShell.singleton = new DebugShell(TabNode); + shell_ = DebugShell.singleton; + + // enable debug output + //shell_.debug = true; + } +}; + +/** + * Print debugging message when DebugShell's debug flag is true. + */ +function dprint(str) { + if (shell_ && shell_.debug) { + print(str); + } +}; + +/** + * Helper that throws error if x is not a number + * @param x {object} - object to test type of + * @param error_message {string} - error to throw on failure + */ +function assertIsNumberType(x, error_message) { + if (typeof x != "number") + throw error_message; +} + +////////////////////// migration staff ////////////////////////// +// This file was copied from chrome\browser\debugger\resources\debugger_shell.js + +function print(txt) { + var ui = DebugShell.ui; + if (ui) { + ui.appendText(txt); + } +} + +var TabNode = { + debugBreak: function(force) { + DebuggerIPC.sendMessage(["debugBreak", force]); + }, + attach: function() { + DebuggerIPC.sendMessage(["attach"]); + }, + detach: function() { + // TODO(yurys): send this from DebugHandler when it's being destroyed? + DebuggerIPC.sendMessage(["detach"]); + }, + sendToDebugger: function(str) { + DebuggerIPC.sendMessage(["sendToDebugger", str]); + } +}; + +var ChromeNode = { + setDebuggerReady: function(isReady) { + DebuggerIPC.sendMessage(["setDebuggerReady", isReady]); + }, + setDebuggerBreak: function(isBreak) { + var ui = DebugShell.ui; + if (ui) { + ui.setDebuggerBreak(isBreak); + } + DebuggerIPC.sendMessage(["setDebuggerBreak", isBreak]); + } +}; diff --git a/webkit/port/page/inspector/debugger.css b/webkit/port/page/inspector/debugger.css new file mode 100644 index 0000000..ed9df2f --- /dev/null +++ b/webkit/port/page/inspector/debugger.css @@ -0,0 +1,35 @@ +/** + * Style for javascript debugger. See debugger.html. + */ + +html,body { + margin: 0px; + padding: 0px; + height: 100%; +} +#output { + font-family: monospace; + background-color: #ffffff; + min-height: 100%; +} +#outer { + height: 100%; + width: 100%; + white-space: pre-wrap; + padding: 0px 0px 24px 0px; +} +#command-line { + bottom: 0px; + /* not quite sure why this 5px is necessary */ + right: 5px; + left: 0px; + position: fixed; + padding: 0px; + margin: 0px; +} +#command-line-text { + height: 20px; + display: block; + width: 100%; + font-family: monospace; +} diff --git a/webkit/port/page/inspector/debugger.html b/webkit/port/page/inspector/debugger.html new file mode 100644 index 0000000..f6bb917 --- /dev/null +++ b/webkit/port/page/inspector/debugger.html @@ -0,0 +1,31 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> +<html> +<!-- + The UI for the javascript debugger window. +--> + <head> + <title>JavaScript Debugger</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF8" /> + <link rel="stylesheet" href="debugger.css" type="text/css" /> + <script type="text/javascript" src="DebuggerConsole.js"></script> + <script type="text/javascript" src="DebuggerIPC.js"></script> + <script type="text/javascript" src="DebuggerShell.js"></script> + </head> + + <body onload="onLoad();"> + + <table id='outer'> + <tr> + <td valign='bottom' id='output'>Chrome JavaScript Debugger<br />Type 'help' for a list of commands.<br /></td> + </tr> + </table> + + <div id='command-line'> + <!-- TODO(erikkay) - use addEventListener instead --> + <input id='command-line-text' + type="text" /> + </div> + + </body> +</html> |