diff options
author | pfeldman@chromium.org <pfeldman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-20 11:38:03 +0000 |
---|---|---|
committer | pfeldman@chromium.org <pfeldman@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-07-20 11:38:03 +0000 |
commit | 0639cfd4ed40aa9450c5781c86d6b47af8396220 (patch) | |
tree | 486f82e04cd53623ffb67ae7932523bde70b118f /chrome/browser/debugger | |
parent | d641b6e4c2ebf556cb42818e7b3470329baf170c (diff) | |
download | chromium_src-0639cfd4ed40aa9450c5781c86d6b47af8396220.zip chromium_src-0639cfd4ed40aa9450c5781c86d6b47af8396220.tar.gz chromium_src-0639cfd4ed40aa9450c5781c86d6b47af8396220.tar.bz2 |
DevTools: bulk move of legacy protocol back to chrome/.
Review URL: http://codereview.chromium.org/7458015
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@93182 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/debugger')
23 files changed, 3151 insertions, 10 deletions
diff --git a/chrome/browser/debugger/browser_list_tabcontents_provider.cc b/chrome/browser/debugger/browser_list_tabcontents_provider.cc new file mode 100644 index 0000000..65addd8 --- /dev/null +++ b/chrome/browser/debugger/browser_list_tabcontents_provider.cc @@ -0,0 +1,20 @@ +// Copyright (c) 2011 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/browser_list_tabcontents_provider.h" + +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/ui/browser_list.h" + +DevToolsHttpProtocolHandler::InspectableTabs +BrowserListTabContentsProvider::GetInspectableTabs() { + DevToolsHttpProtocolHandler::InspectableTabs tabs; + for (BrowserList::const_iterator it = BrowserList::begin(), + end = BrowserList::end(); it != end; ++it) { + TabStripModel* model = (*it)->tabstrip_model(); + for (int i = 0, size = model->count(); i < size; ++i) + tabs.push_back(model->GetTabContentsAt(i)); + } + return tabs; +} diff --git a/chrome/browser/debugger/browser_list_tabcontents_provider.h b/chrome/browser/debugger/browser_list_tabcontents_provider.h new file mode 100644 index 0000000..d8adb9b --- /dev/null +++ b/chrome/browser/debugger/browser_list_tabcontents_provider.h @@ -0,0 +1,21 @@ +// Copyright (c) 2011 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_BROWSER_LIST_TABCONTENTS_PROVIDER_H_ +#define CHROME_BROWSER_DEBUGGER_BROWSER_LIST_TABCONTENTS_PROVIDER_H_ + +#include "content/browser/debugger/devtools_http_protocol_handler.h" + +class BrowserListTabContentsProvider + : public DevToolsHttpProtocolHandler::TabContentsProvider { + public: + BrowserListTabContentsProvider() {} + virtual ~BrowserListTabContentsProvider() {} + + virtual DevToolsHttpProtocolHandler::InspectableTabs GetInspectableTabs(); + private: + DISALLOW_COPY_AND_ASSIGN(BrowserListTabContentsProvider); +}; + +#endif // CHROME_BROWSER_DEBUGGER_BROWSER_LIST_TABCONTENTS_PROVIDER_H_ diff --git a/chrome/browser/debugger/debugger_remote_service.cc b/chrome/browser/debugger/debugger_remote_service.cc new file mode 100644 index 0000000..a93084f --- /dev/null +++ b/chrome/browser/debugger/debugger_remote_service.cc @@ -0,0 +1,337 @@ +// Copyright (c) 2011 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. + +// This file contains implementations of the DebuggerRemoteService methods, +// defines DebuggerRemoteService and DebuggerRemoteServiceCommand constants. + +#include "chrome/browser/debugger/debugger_remote_service.h" + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/stringprintf.h" +#include "base/string_number_conversions.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "chrome/browser/debugger/devtools_protocol_handler.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "chrome/browser/debugger/inspectable_tab_proxy.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "chrome/common/render_messages.h" +#include "content/browser/debugger/devtools_manager.h" +#include "content/browser/renderer_host/render_view_host.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/common/devtools_messages.h" + +namespace { + +// Constants for the "data", "result", and "command" JSON message fields. +const char kDataKey[] = "data"; +const char kResultKey[] = "result"; +const char kCommandKey[] = "command"; + +} // namespace + +const std::string DebuggerRemoteServiceCommand::kAttach = "attach"; +const std::string DebuggerRemoteServiceCommand::kDetach = "detach"; +const std::string DebuggerRemoteServiceCommand::kDebuggerCommand = + "debugger_command"; +const std::string DebuggerRemoteServiceCommand::kEvaluateJavascript = + "evaluate_javascript"; +const std::string DebuggerRemoteServiceCommand::kFrameNavigate = + "navigated"; +const std::string DebuggerRemoteServiceCommand::kTabClosed = + "closed"; + +const std::string DebuggerRemoteService::kToolName = "V8Debugger"; + +DebuggerRemoteService::DebuggerRemoteService(DevToolsProtocolHandler* delegate) + : delegate_(delegate) {} + +DebuggerRemoteService::~DebuggerRemoteService() {} + +// This method handles the V8Debugger tool commands which are +// retrieved from the request "command" field. If an operation result +// is ready off-hand (synchronously), it is sent back to the remote debugger. +// Otherwise the corresponding response is received through IPC from the +// V8 debugger via DevToolsClientHost. +void DebuggerRemoteService::HandleMessage( + const DevToolsRemoteMessage& message) { + const std::string destination = message.destination(); + scoped_ptr<Value> request(base::JSONReader::Read(message.content(), true)); + if (request.get() == NULL) { + // Bad JSON + NOTREACHED(); + return; + } + DictionaryValue* content; + if (!request->IsType(Value::TYPE_DICTIONARY)) { + NOTREACHED(); // Broken protocol :( + return; + } + content = static_cast<DictionaryValue*>(request.get()); + if (!content->HasKey(kCommandKey)) { + NOTREACHED(); // Broken protocol :( + return; + } + std::string command; + DictionaryValue response; + + content->GetString(kCommandKey, &command); + response.SetString(kCommandKey, command); + bool send_response = true; + if (destination.empty()) { + // Unknown command (bad format?) + NOTREACHED(); + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + SendResponse(response, message.tool(), message.destination()); + return; + } + int32 tab_uid = -1; + base::StringToInt(destination, &tab_uid); + + if (command == DebuggerRemoteServiceCommand::kAttach) { + // TODO(apavlov): handle 0 for a new tab + response.SetString(kCommandKey, DebuggerRemoteServiceCommand::kAttach); + AttachToTab(destination, &response); + } else if (command == DebuggerRemoteServiceCommand::kDetach) { + response.SetString(kCommandKey, DebuggerRemoteServiceCommand::kDetach); + DetachFromTab(destination, &response); + } else if (command == DebuggerRemoteServiceCommand::kDebuggerCommand) { + send_response = DispatchDebuggerCommand(tab_uid, content, &response); + } else if (command == DebuggerRemoteServiceCommand::kEvaluateJavascript) { + send_response = DispatchEvaluateJavascript(tab_uid, content, &response); + } else { + // Unknown command + NOTREACHED(); + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + } + + if (send_response) { + SendResponse(response, message.tool(), message.destination()); + } +} + +void DebuggerRemoteService::OnConnectionLost() { + delegate_->inspectable_tab_proxy()->OnRemoteDebuggerDetached(); +} + +// Sends a JSON response to the remote debugger using |response| as content, +// |tool| and |destination| as the respective header values. +void DebuggerRemoteService::SendResponse(const Value& response, + const std::string& tool, + const std::string& destination) { + std::string response_content; + base::JSONWriter::Write(&response, false, &response_content); + scoped_ptr<DevToolsRemoteMessage> response_message( + DevToolsRemoteMessageBuilder::instance().Create(tool, + destination, + response_content)); + delegate_->Send(*response_message.get()); +} + +// Gets a TabContents instance corresponding to the |tab_uid| using the +// InspectableTabProxy controllers map, or NULL if none found. +TabContents* DebuggerRemoteService::ToTabContents(int32 tab_uid) { + const InspectableTabProxy::TabMap& tab_map = + delegate_->inspectable_tab_proxy()->tab_map(); + InspectableTabProxy::TabMap::const_iterator it = tab_map.find(tab_uid); + if (it != tab_map.end()) { + TabContents* tab_contents = it->second->tab_contents(); + if (tab_contents == NULL) { + return NULL; + } else { + return tab_contents; + } + } else { + return NULL; + } +} + +// Gets invoked from a DevToolsClientHost callback whenever +// a message from the V8 VM debugger corresponding to |tab_id| is received. +// Composes a Chrome Developer Tools Protocol JSON response and sends it +// to the remote debugger. +void DebuggerRemoteService::DebuggerOutput(int32 tab_uid, + const std::string& message) { + std::string content = StringPrintf( + "{\"command\":\"%s\",\"result\":%s,\"data\":%s}", + DebuggerRemoteServiceCommand::kDebuggerCommand.c_str(), + base::IntToString(RESULT_OK).c_str(), + message.c_str()); + scoped_ptr<DevToolsRemoteMessage> response_message( + DevToolsRemoteMessageBuilder::instance().Create( + kToolName, + base::IntToString(tab_uid), + content)); + delegate_->Send(*(response_message.get())); +} + +// Gets invoked from a DevToolsClientHost callback whenever +// a tab corresponding to |tab_id| changes its URL. |url| is the new +// URL of the tab (may be the same as the previous one if the tab is reloaded). +// Sends the corresponding message to the remote debugger. +void DebuggerRemoteService::FrameNavigate(int32 tab_uid, + const std::string& url) { + DictionaryValue value; + value.SetString(kCommandKey, DebuggerRemoteServiceCommand::kFrameNavigate); + value.SetInteger(kResultKey, RESULT_OK); + value.SetString(kDataKey, url); + SendResponse(value, kToolName, base::IntToString(tab_uid)); +} + +// Gets invoked from a DevToolsClientHost callback whenever +// a tab corresponding to |tab_id| gets closed. +// Sends the corresponding message to the remote debugger. +void DebuggerRemoteService::TabClosed(int32 tab_id) { + DictionaryValue value; + value.SetString(kCommandKey, DebuggerRemoteServiceCommand::kTabClosed); + value.SetInteger(kResultKey, RESULT_OK); + SendResponse(value, kToolName, base::IntToString(tab_id)); +} + +// Attaches a remote debugger to the target tab specified by |destination| +// by posting the DevToolsAgentMsg_Attach message and sends a response +// to the remote debugger immediately. +void DebuggerRemoteService::AttachToTab(const std::string& destination, + DictionaryValue* response) { + int32 tab_uid = -1; + base::StringToInt(destination, &tab_uid); + if (tab_uid < 0) { + // Bad tab_uid received from remote debugger (perhaps NaN) + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return; + } + if (tab_uid == 0) { // single tab_uid + // We've been asked to open a new tab with URL + // TODO(apavlov): implement + NOTIMPLEMENTED(); + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return; + } + TabContents* tab_contents = ToTabContents(tab_uid); + if (tab_contents == NULL) { + // No active tab contents with tab_uid + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return; + } + RenderViewHost* target_host = tab_contents->render_view_host(); + DevToolsClientHost* client_host = + delegate_->inspectable_tab_proxy()->ClientHostForTabId(tab_uid); + if (client_host == NULL) { + client_host = + delegate_->inspectable_tab_proxy()->NewClientHost(tab_uid, this); + DevToolsManager* manager = DevToolsManager::GetInstance(); + if (manager != NULL) { + manager->RegisterDevToolsClientHostFor(target_host, client_host); + response->SetInteger(kResultKey, RESULT_OK); + } else { + response->SetInteger(kResultKey, RESULT_DEBUGGER_ERROR); + } + } else { + // DevToolsClientHost for this tab is already registered + response->SetInteger(kResultKey, RESULT_ILLEGAL_TAB_STATE); + } +} + +// Detaches a remote debugger from the target tab specified by |destination| +// by posting the DevToolsAgentMsg_Detach message and sends a response +// to the remote debugger immediately. +void DebuggerRemoteService::DetachFromTab(const std::string& destination, + DictionaryValue* response) { + int32 tab_uid = -1; + base::StringToInt(destination, &tab_uid); + if (tab_uid == -1) { + // Bad tab_uid received from remote debugger (NaN) + if (response != NULL) { + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + } + return; + } + int result_code; + DevToolsClientHostImpl* client_host = + delegate_->inspectable_tab_proxy()->ClientHostForTabId(tab_uid); + if (client_host != NULL) { + client_host->CloseImpl(); + result_code = RESULT_OK; + } else { + // No client host registered for |tab_uid|. + result_code = RESULT_UNKNOWN_TAB; + } + if (response != NULL) { + response->SetInteger(kResultKey, result_code); + } +} + +// Sends a V8 debugger command to the target tab V8 debugger. +// Does not send back a response (which is received asynchronously +// through IPC) unless an error occurs before the command has actually +// been sent. +bool DebuggerRemoteService::DispatchDebuggerCommand(int tab_uid, + DictionaryValue* content, + DictionaryValue* response) { + if (tab_uid == -1) { + // Invalid tab_uid from remote debugger (perhaps NaN) + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return true; + } + DevToolsManager* manager = DevToolsManager::GetInstance(); + if (manager == NULL) { + response->SetInteger(kResultKey, RESULT_DEBUGGER_ERROR); + return true; + } + TabContents* tab_contents = ToTabContents(tab_uid); + if (tab_contents == NULL) { + // Unknown tab_uid from remote debugger + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return true; + } + DevToolsClientHost* client_host = + manager->GetDevToolsClientHostFor(tab_contents->render_view_host()); + if (client_host == NULL) { + // tab_uid is not being debugged (Attach has not been invoked) + response->SetInteger(kResultKey, RESULT_ILLEGAL_TAB_STATE); + return true; + } + std::string v8_command; + DictionaryValue* v8_command_value; + content->GetDictionary(kDataKey, &v8_command_value); + base::JSONWriter::Write(v8_command_value, false, &v8_command); + manager->ForwardToDevToolsAgent( + client_host, DevToolsAgentMsg_DebuggerCommand(MSG_ROUTING_NONE, + v8_command)); + // Do not send the response right now, as the JSON will be received from + // the V8 debugger asynchronously. + return false; +} + +// Sends the immediate "evaluate Javascript" command to the V8 debugger. +// The evaluation result is not sent back to the client as this command +// is in fact needed to invoke processing of queued debugger commands. +bool DebuggerRemoteService::DispatchEvaluateJavascript( + int tab_uid, + DictionaryValue* content, + DictionaryValue* response) { + if (tab_uid == -1) { + // Invalid tab_uid from remote debugger (perhaps NaN) + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return true; + } + TabContents* tab_contents = ToTabContents(tab_uid); + if (tab_contents == NULL) { + // Unknown tab_uid from remote debugger + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return true; + } + RenderViewHost* render_view_host = tab_contents->render_view_host(); + if (render_view_host == NULL) { + // No RenderViewHost + response->SetInteger(kResultKey, RESULT_UNKNOWN_TAB); + return true; + } + std::string javascript; + content->GetString(kDataKey, &javascript); + render_view_host->ExecuteJavascriptInWebFrame(string16(), + UTF8ToUTF16(javascript)); + return false; +} diff --git a/chrome/browser/debugger/debugger_remote_service.h b/chrome/browser/debugger/debugger_remote_service.h new file mode 100644 index 0000000..113fc8f --- /dev/null +++ b/chrome/browser/debugger/debugger_remote_service.h @@ -0,0 +1,127 @@ +// Copyright (c) 2011 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. + +// This file declares the DebuggerRemoteServiceCommand struct and the +// DebuggerRemoteService class which handles commands directed to the +// "V8Debugger" tool. +#ifndef CHROME_BROWSER_DEBUGGER_DEBUGGER_REMOTE_SERVICE_H_ +#define CHROME_BROWSER_DEBUGGER_DEBUGGER_REMOTE_SERVICE_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "chrome/browser/debugger/devtools_remote.h" + +class DevToolsProtocolHandler; +class DevToolsRemoteMessage; +class TabContents; + +namespace base { +class DictionaryValue; +class Value; +} + +// Contains constants for DebuggerRemoteService tool protocol commands +// (V8-related only). +struct DebuggerRemoteServiceCommand { + static const std::string kAttach; + static const std::string kDetach; + static const std::string kDebuggerCommand; + static const std::string kEvaluateJavascript; + static const std::string kFrameNavigate; // navigation event + static const std::string kTabClosed; // tab closing event +}; + +// Handles V8 debugger-related messages from the remote debugger (like +// attach to V8 debugger, detach from V8 debugger, send command to V8 debugger) +// and proxies JSON messages from V8 debugger to the remote debugger. +class DebuggerRemoteService : public DevToolsRemoteListener { + public: + // |delegate| (never NULL) is the protocol handler instance + // which dispatches messages to this service. The responses from the + // V8 VM debugger are routed back to |delegate|. + // The ownership of |delegate| is NOT transferred to this class. + explicit DebuggerRemoteService(DevToolsProtocolHandler* delegate); + + // Handles a JSON message from the tab_uid-associated V8 debugger. + void DebuggerOutput(int32 tab_uid, const std::string& message); + + // Handles a frame navigation event. + void FrameNavigate(int32 tab_uid, const std::string& url); + + // Handles a tab closing event. + void TabClosed(int32 tab_uid); + + // Detaches the remote debugger from the tab specified by |destination|. + // It is public so that we can detach from the tab on the remote debugger + // connection loss. + // If |response| is not NULL, the operation result will be written + // as the "result" field in |response|, otherwise the result + // will not be propagated back to the caller. + void DetachFromTab(const std::string& destination, + base::DictionaryValue* response); + + // DevToolsRemoteListener interface. + + // Processes |message| from the remote debugger, where the tool is + // "V8Debugger". Either sends the reply immediately or waits for an + // asynchronous response from the V8 debugger. + virtual void HandleMessage(const DevToolsRemoteMessage& message); + + // Gets invoked on the remote debugger [socket] connection loss. + // Notifies the InspectableTabProxy of the remote debugger detachment. + virtual void OnConnectionLost(); + + // Specifies a tool name ("V8Debugger") handled by this class. + static const std::string kToolName; + + private: + // Operation result returned in the "result" field. + typedef enum { + RESULT_OK = 0, + RESULT_ILLEGAL_TAB_STATE, + RESULT_UNKNOWN_TAB, + RESULT_DEBUGGER_ERROR, + RESULT_UNKNOWN_COMMAND + } Result; + + virtual ~DebuggerRemoteService(); + + // Attaches a remote debugger to the tab specified by |destination|. + // Writes the attachment result (one of Result enum values) into |response|. + void AttachToTab(const std::string& destination, + base::DictionaryValue* response); + + // Retrieves a WebContents instance for the specified |tab_uid| + // or NULL if no such tab is found or no WebContents instance + // corresponds to that tab. + TabContents* ToTabContents(int32 tab_uid); + + // Sends a JSON message with the |response| to the remote debugger. + // |tool| and |destination| are used as the respective header values. + void SendResponse(const base::Value& response, + const std::string& tool, + const std::string& destination); + + // Redirects a V8 debugger command from |content| to a V8 debugger associated + // with the |tab_uid| and writes the result into |response| if it becomes + // known immediately. + bool DispatchDebuggerCommand(int tab_uid, + base::DictionaryValue* content, + base::DictionaryValue* response); + + // Redirects a Javascript evaluation command from |content| to + // a V8 debugger associated with the |tab_uid| and writes the result + // into |response| if it becomes known immediately. + bool DispatchEvaluateJavascript(int tab_uid, + base::DictionaryValue* content, + base::DictionaryValue* response); + + // The delegate is used to get an InspectableTabProxy instance. + DevToolsProtocolHandler* delegate_; + DISALLOW_COPY_AND_ASSIGN(DebuggerRemoteService); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEBUGGER_REMOTE_SERVICE_H_ diff --git a/chrome/browser/debugger/devtools_protocol_handler.cc b/chrome/browser/debugger/devtools_protocol_handler.cc new file mode 100644 index 0000000..2245a96 --- /dev/null +++ b/chrome/browser/debugger/devtools_protocol_handler.cc @@ -0,0 +1,126 @@ +// Copyright (c) 2011 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/devtools_protocol_handler.h" + +#include "base/logging.h" +#include "chrome/browser/debugger/inspectable_tab_proxy.h" +#include "chrome/browser/debugger/debugger_remote_service.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "chrome/browser/debugger/devtools_remote_listen_socket.h" +#include "chrome/browser/debugger/devtools_remote_service.h" +#include "chrome/browser/debugger/extension_ports_remote_service.h" +#include "content/browser/browser_thread.h" + +// static +scoped_refptr<DevToolsProtocolHandler> DevToolsProtocolHandler::Start( + int port) { + scoped_refptr<DevToolsProtocolHandler> proto_handler = + new DevToolsProtocolHandler(port); + proto_handler->RegisterDestination( + new DevToolsRemoteService(proto_handler), + DevToolsRemoteService::kToolName); + proto_handler->RegisterDestination( + new DebuggerRemoteService(proto_handler), + DebuggerRemoteService::kToolName); + proto_handler->RegisterDestination( + new ExtensionPortsRemoteService(proto_handler), + ExtensionPortsRemoteService::kToolName); + proto_handler->Start(); + return proto_handler; +} + +DevToolsProtocolHandler::DevToolsProtocolHandler(int port) + : port_(port), + connection_(NULL), + server_(NULL) { + inspectable_tab_proxy_.reset(new InspectableTabProxy); +} + +DevToolsProtocolHandler::~DevToolsProtocolHandler() { + // Stop() must be called prior to this being called + DCHECK(server_.get() == NULL); + DCHECK(connection_.get() == NULL); +} + +void DevToolsProtocolHandler::Start() { + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + NewRunnableMethod(this, &DevToolsProtocolHandler::Init)); +} + +void DevToolsProtocolHandler::Init() { + server_ = DevToolsRemoteListenSocket::Listen( + "127.0.0.1", port_, this); +} + +void DevToolsProtocolHandler::Stop() { + BrowserThread::PostTask( + BrowserThread::IO, FROM_HERE, + NewRunnableMethod(this, &DevToolsProtocolHandler::Teardown)); + tool_to_listener_map_.clear(); // Releases all scoped_refptr's to listeners +} + +// Run in I/O thread +void DevToolsProtocolHandler::Teardown() { + connection_ = NULL; + server_ = NULL; +} + +void DevToolsProtocolHandler::RegisterDestination( + DevToolsRemoteListener* listener, + const std::string& tool_name) { + DCHECK(tool_to_listener_map_.find(tool_name) == tool_to_listener_map_.end()); + tool_to_listener_map_.insert(std::make_pair(tool_name, listener)); +} + +void DevToolsProtocolHandler::UnregisterDestination( + DevToolsRemoteListener* listener, + const std::string& tool_name) { + DCHECK(tool_to_listener_map_.find(tool_name) != tool_to_listener_map_.end()); + DCHECK(tool_to_listener_map_.find(tool_name)->second == listener); + tool_to_listener_map_.erase(tool_name); +} + +void DevToolsProtocolHandler::HandleMessage( + const DevToolsRemoteMessage& message) { + std::string tool = message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kTool); + ToolToListenerMap::const_iterator it = tool_to_listener_map_.find(tool); + if (it == tool_to_listener_map_.end()) { + NOTREACHED(); // an unsupported tool, bail out + return; + } + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + NewRunnableMethod( + it->second.get(), &DevToolsRemoteListener::HandleMessage, message)); +} + +void DevToolsProtocolHandler::Send(const DevToolsRemoteMessage& message) { + if (connection_ != NULL) { + connection_->Send(message.ToString()); + } +} + +void DevToolsProtocolHandler::OnAcceptConnection( + net::ListenSocket *connection) { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + connection_ = connection; +} + +void DevToolsProtocolHandler::OnConnectionLost() { + DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); + connection_ = NULL; + for (ToolToListenerMap::const_iterator it = tool_to_listener_map_.begin(), + end = tool_to_listener_map_.end(); + it != end; + ++it) { + BrowserThread::PostTask( + BrowserThread::UI, FROM_HERE, + NewRunnableMethod( + it->second.get(), &DevToolsRemoteListener::OnConnectionLost)); + } +} diff --git a/chrome/browser/debugger/devtools_protocol_handler.h b/chrome/browser/debugger/devtools_protocol_handler.h new file mode 100644 index 0000000..b1b9158d --- /dev/null +++ b/chrome/browser/debugger/devtools_protocol_handler.h @@ -0,0 +1,76 @@ +// Copyright (c) 2011 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_DEVTOOLS_PROTOCOL_HANDLER_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_PROTOCOL_HANDLER_H_ +#pragma once + +#include <string> + +#include "base/hash_tables.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "chrome/browser/debugger/devtools_remote.h" +#include "net/base/listen_socket.h" + +class InspectableTabProxy; +class DevToolsRemoteListenSocket; +class DevToolsRemoteMessage; + +// Dispatches DevToolsRemoteMessages to their appropriate handlers (Tools) +// based on the "Tool" message header value. +class DevToolsProtocolHandler + : public DevToolsRemoteListener, + public OutboundSocketDelegate { + public: + typedef base::hash_map< std::string, scoped_refptr<DevToolsRemoteListener> > + ToolToListenerMap; + + static scoped_refptr<DevToolsProtocolHandler> Start(int port); + + // Called from the main thread in order to stop protocol handler. + // Will schedule tear down task on IO thread. + void Stop(); + + // Registers a |listener| to handle messages for a certain |tool_name| Tool. + // |listener| is the new message handler to register. + // As DevToolsRemoteListener inherits base::RefCountedThreadSafe, + // you should have no problems with ownership and destruction. + // |tool_name| is the name of the Tool to associate the listener with. + void RegisterDestination(DevToolsRemoteListener* listener, + const std::string& tool_name); + + // Unregisters a |listener| so that it will no longer handle messages + // directed to the specified |tool_name| tool. + void UnregisterDestination(DevToolsRemoteListener* listener, + const std::string& tool_name); + + InspectableTabProxy* inspectable_tab_proxy() { + return inspectable_tab_proxy_.get(); + } + + // DevToolsRemoteListener interface + virtual void HandleMessage(const DevToolsRemoteMessage& message); + virtual void OnAcceptConnection(net::ListenSocket *connection); + virtual void OnConnectionLost(); + + // OutboundSocketDelegate interface + virtual void Send(const DevToolsRemoteMessage& message); + + private: + explicit DevToolsProtocolHandler(int port); + virtual ~DevToolsProtocolHandler(); + void Start(); + + void Init(); + void Teardown(); + int port_; + ToolToListenerMap tool_to_listener_map_; + scoped_refptr<net::ListenSocket> connection_; + scoped_refptr<DevToolsRemoteListenSocket> server_; + scoped_ptr<InspectableTabProxy> inspectable_tab_proxy_; + DISALLOW_COPY_AND_ASSIGN(DevToolsProtocolHandler); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_PROTOCOL_HANDLER_H_ diff --git a/chrome/browser/debugger/devtools_remote.h b/chrome/browser/debugger/devtools_remote.h new file mode 100644 index 0000000..78b1d13 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote.h @@ -0,0 +1,47 @@ +// Copyright (c) 2011 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_DEVTOOLS_REMOTE_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" + +class DevToolsRemoteMessage; + +namespace net { +class ListenSocket; +} + +// This interface should be implemented by a class that wants to handle +// DevToolsRemoteMessages dispatched by some entity. It must extend +class DevToolsRemoteListener + : public base::RefCountedThreadSafe<DevToolsRemoteListener> { + public: + DevToolsRemoteListener() {} + virtual void HandleMessage(const DevToolsRemoteMessage& message) = 0; + // This method is invoked on the UI thread whenever the debugger connection + // has been lost. + virtual void OnConnectionLost() = 0; + virtual void OnAcceptConnection(net::ListenSocket* connection) {} + + protected: + friend class base::RefCountedThreadSafe<DevToolsRemoteListener>; + + virtual ~DevToolsRemoteListener() {} + + private: + DISALLOW_COPY_AND_ASSIGN(DevToolsRemoteListener); +}; + +// Interface exposed by DevToolsProtocolHandler to receive reply messages +// from registered tools. +class OutboundSocketDelegate { + public: + virtual ~OutboundSocketDelegate() {} + virtual void Send(const DevToolsRemoteMessage& message) = 0; +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_H_ diff --git a/chrome/browser/debugger/devtools_remote_listen_socket.cc b/chrome/browser/debugger/devtools_remote_listen_socket.cc new file mode 100644 index 0000000..0ec9e32 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_listen_socket.cc @@ -0,0 +1,258 @@ +// Copyright (c) 2011 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/devtools_remote_listen_socket.h" + +#include "build/build_config.h" + +#include <stdlib.h> + +#if defined(OS_WIN) +// winsock2.h must be included first in order to ensure it is included before +// windows.h. +#include <winsock2.h> +#elif defined(OS_POSIX) +#include <errno.h> +#include <sys/socket.h> +#endif + +#include "base/compiler_specific.h" +#include "base/eintr_wrapper.h" +#include "base/string_number_conversions.h" +#include "base/threading/platform_thread.h" +#include "chrome/browser/debugger/devtools_remote.h" +#include "chrome/browser/debugger/devtools_remote_message.h" + +#define CONSUME_BUFFER_CHAR \ + pBuf++;\ + len-- + +#if defined(OS_POSIX) +// Used same name as in Windows to avoid #ifdef where refrenced +#define SOCKET int +const int INVALID_SOCKET = -1; +const int SOCKET_ERROR = -1; +#endif + +const int kReadBufSize = 200; + +DevToolsRemoteListenSocket::DevToolsRemoteListenSocket( + SOCKET s, + DevToolsRemoteListener* message_listener) + : ALLOW_THIS_IN_INITIALIZER_LIST(net::ListenSocket(s, this)), + state_(HANDSHAKE), + remaining_payload_length_(0), + message_listener_(message_listener), + cr_received_(false) {} + +void DevToolsRemoteListenSocket::StartNextField() { + switch (state_) { + case INVALID: + state_ = HANDSHAKE; + break; + case HANDSHAKE: + state_ = HEADERS; + break; + case HEADERS: + if (protocol_field_.empty()) { // empty line - end of headers + const std::string& payload_length_string = GetHeader( + DevToolsRemoteMessageHeaders::kContentLength, "0"); + base::StringToInt(payload_length_string, &remaining_payload_length_); + state_ = PAYLOAD; + if (remaining_payload_length_ == 0) { // no payload + DispatchField(); + return; + } + } + break; + case PAYLOAD: + header_map_.clear(); + payload_.clear(); + state_ = HEADERS; + break; + default: + NOTREACHED(); + break; + } + protocol_field_.clear(); +} + +DevToolsRemoteListenSocket::~DevToolsRemoteListenSocket() {} + +DevToolsRemoteListenSocket* + DevToolsRemoteListenSocket::Listen(const std::string& ip, + int port, + DevToolsRemoteListener* listener) { + SOCKET s = net::ListenSocket::Listen(ip, port); + if (s == INVALID_SOCKET) { + // TODO(apavlov): error handling + } else { + DevToolsRemoteListenSocket* sock = + new DevToolsRemoteListenSocket(s, listener); + sock->Listen(); + return sock; + } + return NULL; +} + +void DevToolsRemoteListenSocket::DidAccept(net::ListenSocket *server, + net::ListenSocket *connection) { + connection->AddRef(); + message_listener_->OnAcceptConnection(connection); +} + +// Dispatches data from socket to socket_delegate_, extracting messages +// delimited by newlines. +void DevToolsRemoteListenSocket::DidRead(net::ListenSocket* connection, + const char* pBuf, + int len) { + while (len > 0) { + if (state_ != PAYLOAD) { + if (cr_received_ && *pBuf == '\n') { + cr_received_ = false; + CONSUME_BUFFER_CHAR; + } else { + while (*pBuf != '\r' && len > 0) { + protocol_field_.push_back(*pBuf); + CONSUME_BUFFER_CHAR; + } + if (*pBuf == '\r') { + cr_received_ = true; + CONSUME_BUFFER_CHAR; + } + continue; + } + switch (state_) { + case HANDSHAKE: + case HEADERS: + DispatchField(); + break; + default: + NOTREACHED(); + break; + } + } else { // PAYLOAD + while (remaining_payload_length_ > 0 && len > 0) { + protocol_field_.push_back(*pBuf); + CONSUME_BUFFER_CHAR; + remaining_payload_length_--; + } + if (remaining_payload_length_ == 0) { + DispatchField(); + } + } + } +} + +void DevToolsRemoteListenSocket::DidClose(net::ListenSocket *connection) { + message_listener_->OnConnectionLost(); + connection->Release(); +} + +void DevToolsRemoteListenSocket::DispatchField() { + static const std::string kHandshakeString = "ChromeDevToolsHandshake"; + switch (state_) { + case HANDSHAKE: + if (protocol_field_.compare(kHandshakeString)) { + state_ = INVALID; + } else { + Send(kHandshakeString, true); + } + break; + case HEADERS: { + if (!protocol_field_.empty()) { // not end-of-headers + std::string::size_type colon_pos = protocol_field_.find_first_of(":"); + if (colon_pos == std::string::npos) { + // TODO(apavlov): handle the error (malformed header) + } else { + const std::string header_name = protocol_field_.substr(0, colon_pos); + std::string header_val = protocol_field_.substr(colon_pos + 1); + header_map_[header_name] = header_val; + } + } + break; + } + case PAYLOAD: + payload_ = protocol_field_; + HandleMessage(); + break; + default: + NOTREACHED(); + break; + } + StartNextField(); +} + +const std::string& DevToolsRemoteListenSocket::GetHeader( + const std::string& header_name, + const std::string& default_value) const { + DevToolsRemoteMessage::HeaderMap::const_iterator it = + header_map_.find(header_name); + if (it == header_map_.end()) { + return default_value; + } + return it->second; +} + +// Handle header_map_ and payload_ +void DevToolsRemoteListenSocket::HandleMessage() { + if (message_listener_ != NULL) { + DevToolsRemoteMessage message(header_map_, payload_); + message_listener_->HandleMessage(message); + } +} + +void DevToolsRemoteListenSocket::Listen() { + net::ListenSocket::Listen(); +} + +void DevToolsRemoteListenSocket::Accept() { + SOCKET conn = net::ListenSocket::Accept(socket_); + if (conn != INVALID_SOCKET) { + scoped_refptr<DevToolsRemoteListenSocket> sock( + new DevToolsRemoteListenSocket(conn, + message_listener_)); + // it's up to the delegate to AddRef if it wants to keep it around +#if defined(OS_POSIX) + sock->WatchSocket(WAITING_READ); +#endif + socket_delegate_->DidAccept(this, sock); + } else { + // TODO(apavlov): some error handling required here + } +} + +void DevToolsRemoteListenSocket::SendInternal(const char* bytes, int len) { + char* send_buf = const_cast<char *>(bytes); + int len_left = len; + while (true) { + int sent = HANDLE_EINTR(send(socket_, send_buf, len_left, 0)); + if (sent == len_left) { // A shortcut to avoid extraneous checks. + break; + } + if (sent == kSocketError) { +#if defined(OS_WIN) + if (WSAGetLastError() != WSAEWOULDBLOCK) { + LOG(ERROR) << "send failed: WSAGetLastError()==" << WSAGetLastError(); +#elif defined(OS_POSIX) + if (errno != EWOULDBLOCK && errno != EAGAIN) { + LOG(ERROR) << "send failed: errno==" << errno; +#endif + break; + } + // Otherwise we would block, and now we have to wait for a retry. + // Fall through to PlatformThread::YieldCurrentThread() + } else { + // sent != len_left according to the shortcut above. + // Shift the buffer start and send the remainder after a short while. + send_buf += sent; + len_left -= sent; + } + base::PlatformThread::YieldCurrentThread(); + } +} + +void DevToolsRemoteListenSocket::Close() { + net::ListenSocket::Close(); +} diff --git a/chrome/browser/debugger/devtools_remote_listen_socket.h b/chrome/browser/debugger/devtools_remote_listen_socket.h new file mode 100644 index 0000000..40212f4 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_listen_socket.h @@ -0,0 +1,72 @@ +// Copyright (c) 2011 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_DEVTOOLS_REMOTE_LISTEN_SOCKET_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_LISTEN_SOCKET_H_ +#pragma once + +#include <string> + +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "net/base/listen_socket.h" + +class DevToolsRemoteListener; + +// Listens to remote debugger incoming connections, handles the V8ARDP protocol +// socket input and invokes the message handler when appropriate. +class DevToolsRemoteListenSocket + : public net::ListenSocket, + public net::ListenSocket::ListenSocketDelegate { + public: + // Listen on port for the specified IP address. Use 127.0.0.1 to only + // accept local connections. + static DevToolsRemoteListenSocket* Listen( + const std::string& ip, + int port, + DevToolsRemoteListener* message_listener); + + protected: + virtual void Listen(); + virtual void Accept(); + virtual void Close(); + virtual void SendInternal(const char* bytes, int len); + + private: + virtual ~DevToolsRemoteListenSocket(); + + // net::ListenSocket::ListenSocketDelegate interface + virtual void DidAccept(net::ListenSocket *server, + net::ListenSocket *connection); + virtual void DidRead(net::ListenSocket *connection, + const char* data, int len); + virtual void DidClose(net::ListenSocket *connection); + + // The protocol states while reading socket input + enum State { + INVALID = 0, // Bad handshake message received, retry + HANDSHAKE = 1, // Receiving handshake message + HEADERS = 2, // Receiving protocol headers + PAYLOAD = 3 // Receiving payload + }; + + DevToolsRemoteListenSocket(SOCKET s, + DevToolsRemoteListener *listener); + void StartNextField(); + void HandleMessage(); + void DispatchField(); + const std::string& GetHeader(const std::string& header_name, + const std::string& default_value) const; + + State state_; + DevToolsRemoteMessage::HeaderMap header_map_; + std::string protocol_field_; + std::string payload_; + int32 remaining_payload_length_; + DevToolsRemoteListener* message_listener_; + bool cr_received_; + + DISALLOW_COPY_AND_ASSIGN(DevToolsRemoteListenSocket); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_LISTEN_SOCKET_H_ diff --git a/chrome/browser/debugger/devtools_remote_listen_socket_unittest.cc b/chrome/browser/debugger/devtools_remote_listen_socket_unittest.cc new file mode 100644 index 0000000..9821b59 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_listen_socket_unittest.cc @@ -0,0 +1,383 @@ +// Copyright (c) 2011 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/devtools_remote_listen_socket_unittest.h" + +#include <fcntl.h> +#if defined(OS_POSIX) +#include <netinet/in.h> +#endif + +#include "base/eintr_wrapper.h" +#include "base/test/test_timeouts.h" +#include "base/threading/platform_thread.h" +#include "net/base/net_util.h" +#include "testing/platform_test.h" + +const int DevToolsRemoteListenSocketTester::kTestPort = 9999; + +static const int kReadBufSize = 1024; +static const char* kChromeDevToolsHandshake = "ChromeDevToolsHandshake\r\n"; +static const char* kSimpleMessagePart1 = + "Tool:V8Debugger\r\n" + "Destination:2\r"; +static const char* kSimpleMessagePart2 = + "\n" + "Content-Length:0\r\n" + "\r\n"; +static const char* kTwoMessages = + "Tool:DevToolsService\r\n" + "Content-Length:300\r\n" + "\r\n" + "00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000" + "00000000000000000000000000000000000000000000000000" + "Tool:V8Debugger\r\n" + "Destination:1\r\n" + "Content-Length:0\r\n" + "\r\n"; + +static const int kMaxQueueSize = 20; +static const char* kLoopback = "127.0.0.1"; +#if defined(OS_POSIX) +static const char* kSemaphoreName = "chromium.listen_socket"; +#endif + + +ListenSocketTestAction::ListenSocketTestAction() : action_(ACTION_NONE) {} + +ListenSocketTestAction::ListenSocketTestAction(ActionType action) + : action_(action) {} + +ListenSocketTestAction::ListenSocketTestAction(ActionType action, + std::string data) + : action_(action), + data_(data) {} + +ListenSocketTestAction::ListenSocketTestAction( + ActionType action, + const DevToolsRemoteMessage& message) + : action_(action), + message_(message) {} + +ListenSocketTestAction::~ListenSocketTestAction() {} + +net::ListenSocket* DevToolsRemoteListenSocketTester::DoListen() { + return DevToolsRemoteListenSocket::Listen(kLoopback, kTestPort, this); +} + +DevToolsRemoteListenSocketTester::DevToolsRemoteListenSocketTester() + : semaphore_(NULL), + thread_(NULL), + loop_(NULL), + server_(NULL), + connection_(NULL), + test_socket_(INVALID_SOCKET) { + memset(&lock_, 0, sizeof(lock_)); +} + +void DevToolsRemoteListenSocketTester::SetUp() { +#if defined(OS_WIN) + InitializeCriticalSection(&lock_); + semaphore_ = CreateSemaphore(NULL, 0, kMaxQueueSize, NULL); + server_ = NULL; + net::EnsureWinsockInit(); +#elif defined(OS_POSIX) + ASSERT_EQ(0, pthread_mutex_init(&lock_, NULL)); + sem_unlink(kSemaphoreName); + semaphore_ = sem_open(kSemaphoreName, O_CREAT, 0, 0); + ASSERT_NE(SEM_FAILED, semaphore_); +#endif + base::Thread::Options options; + options.message_loop_type = MessageLoop::TYPE_IO; + thread_.reset(new base::Thread("socketio_test")); + thread_->StartWithOptions(options); + loop_ = static_cast<MessageLoopForIO*>(thread_->message_loop()); + + loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &DevToolsRemoteListenSocketTester::Listen)); + + // verify Listen succeeded + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_FALSE(server_ == NULL); + ASSERT_EQ(ACTION_LISTEN, last_action_.type()); + + // verify the connect/accept and setup test_socket_ + test_socket_ = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, test_socket_); + struct sockaddr_in client; + client.sin_family = AF_INET; + client.sin_addr.s_addr = inet_addr(kLoopback); + client.sin_port = htons(kTestPort); + int ret = HANDLE_EINTR(connect(test_socket_, + reinterpret_cast<sockaddr*>(&client), + sizeof(client))); + ASSERT_NE(ret, SOCKET_ERROR); + + net::SetNonBlocking(test_socket_); + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_ACCEPT, last_action_.type()); +} + +void DevToolsRemoteListenSocketTester::TearDown() { + // verify close +#if defined(OS_WIN) + closesocket(test_socket_); +#elif defined(OS_POSIX) + int ret = HANDLE_EINTR(close(test_socket_)); + ASSERT_EQ(ret, 0); +#endif + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_CLOSE, last_action_.type()); + + loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &DevToolsRemoteListenSocketTester::Shutdown)); + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_SHUTDOWN, last_action_.type()); + +#if defined(OS_WIN) + CloseHandle(semaphore_); + semaphore_ = 0; + DeleteCriticalSection(&lock_); +#elif defined(OS_POSIX) + ASSERT_EQ(0, pthread_mutex_lock(&lock_)); + semaphore_ = NULL; + ASSERT_EQ(0, pthread_mutex_unlock(&lock_)); + ASSERT_EQ(0, sem_unlink(kSemaphoreName)); + ASSERT_EQ(0, pthread_mutex_destroy(&lock_)); +#endif + + thread_.reset(); + loop_ = NULL; +} + +void DevToolsRemoteListenSocketTester::ReportAction( + const ListenSocketTestAction& action) { +#if defined(OS_WIN) + EnterCriticalSection(&lock_); + queue_.push_back(action); + LeaveCriticalSection(&lock_); + ReleaseSemaphore(semaphore_, 1, NULL); +#elif defined(OS_POSIX) + ASSERT_EQ(0, pthread_mutex_lock(&lock_)); + queue_.push_back(action); + ASSERT_EQ(0, pthread_mutex_unlock(&lock_)); + ASSERT_EQ(0, sem_post(semaphore_)); +#endif +} + +bool DevToolsRemoteListenSocketTester::NextAction(int timeout) { +#if defined(OS_WIN) + DWORD ret = ::WaitForSingleObject(semaphore_, timeout); + if (ret != WAIT_OBJECT_0) + return false; + EnterCriticalSection(&lock_); + if (queue_.empty()) { + LeaveCriticalSection(&lock_); + return false; + } + last_action_ = queue_.front(); + queue_.pop_front(); + LeaveCriticalSection(&lock_); + return true; +#elif defined(OS_POSIX) + if (semaphore_ == SEM_FAILED) + return false; + while (true) { + int result = sem_trywait(semaphore_); + base::PlatformThread::Sleep(1); // 1MS sleep + timeout--; + if (timeout <= 0) + return false; + if (result == 0) + break; + } + pthread_mutex_lock(&lock_); + if (queue_.empty()) { + pthread_mutex_unlock(&lock_); + return false; + } + last_action_ = queue_.front(); + queue_.pop_front(); + pthread_mutex_unlock(&lock_); + return true; +#endif +} + +int DevToolsRemoteListenSocketTester::ClearTestSocket() { + char buf[kReadBufSize]; + int len_ret = 0; + int time_out = 0; + do { + int len = HANDLE_EINTR(recv(test_socket_, buf, kReadBufSize, 0)); +#if defined(OS_WIN) + if (len == SOCKET_ERROR) { + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK) { +#elif defined(OS_POSIX) + if (len == SOCKET_ERROR) { + if (errno == EWOULDBLOCK || errno == EAGAIN) { +#endif + base::PlatformThread::Sleep(1); + time_out++; + if (time_out > 10) + break; + continue; // still trying + } + } else if (len == 0) { + // socket closed + break; + } else { + time_out = 0; + len_ret += len; + } + } while (true); + return len_ret; +} + +void DevToolsRemoteListenSocketTester::Shutdown() { + server_->Release(); + server_ = NULL; + ReportAction(ListenSocketTestAction(ACTION_SHUTDOWN)); +} + +void DevToolsRemoteListenSocketTester::Listen() { + server_ = DoListen(); + server_->AddRef(); + ReportAction(ListenSocketTestAction(ACTION_LISTEN)); +} + +void DevToolsRemoteListenSocketTester::SendFromTester() { + connection_->Send(kChromeDevToolsHandshake); + ReportAction(ListenSocketTestAction(ACTION_SEND)); +} + +void DevToolsRemoteListenSocketTester::OnAcceptConnection( + net::ListenSocket* connection) { + connection_ = connection; + ReportAction(ListenSocketTestAction(ACTION_ACCEPT)); +} + +void DevToolsRemoteListenSocketTester::OnConnectionLost() { + connection_ = NULL; + ReportAction(ListenSocketTestAction(ACTION_CLOSE)); +} + +void DevToolsRemoteListenSocketTester::HandleMessage( + const DevToolsRemoteMessage& message) { + ReportAction(ListenSocketTestAction(ACTION_READ_MESSAGE, message)); +} + +bool DevToolsRemoteListenSocketTester::Send(SOCKET sock, + const std::string& str) { + int len = static_cast<int>(str.length()); + int send_len = HANDLE_EINTR(send(sock, str.data(), len, 0)); + if (send_len == SOCKET_ERROR) { + LOG(ERROR) << "send failed: " << errno; + return false; + } else if (send_len != len) { + return false; + } + return true; +} + +void DevToolsRemoteListenSocketTester::TestClientSend() { + ASSERT_TRUE(Send(test_socket_, kChromeDevToolsHandshake)); + { + ASSERT_TRUE(Send(test_socket_, kSimpleMessagePart1)); + // sleep for 10ms to test message split between \r and \n + base::PlatformThread::Sleep(10); + ASSERT_TRUE(Send(test_socket_, kSimpleMessagePart2)); + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_READ_MESSAGE, last_action_.type()); + const DevToolsRemoteMessage& message = last_action_.message(); + ASSERT_STREQ("V8Debugger", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kTool).c_str()); + ASSERT_STREQ("2", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kDestination).c_str()); + ASSERT_STREQ("0", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kContentLength).c_str()); + ASSERT_EQ(0, static_cast<int>(message.content().size())); + } + ASSERT_TRUE(Send(test_socket_, kTwoMessages)); + { + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_READ_MESSAGE, last_action_.type()); + const DevToolsRemoteMessage& message = last_action_.message(); + ASSERT_STREQ("DevToolsService", message.tool().c_str()); + ASSERT_STREQ("", message.destination().c_str()); + ASSERT_EQ(300, message.content_length()); + const std::string& content = message.content(); + ASSERT_EQ(300, static_cast<int>(content.size())); + for (int i = 0; i < 300; ++i) { + ASSERT_EQ('0', content[i]); + } + } + { + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_READ_MESSAGE, last_action_.type()); + const DevToolsRemoteMessage& message = last_action_.message(); + ASSERT_STREQ("V8Debugger", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kTool).c_str()); + ASSERT_STREQ("1", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kDestination).c_str()); + ASSERT_STREQ("0", message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kContentLength).c_str()); + const std::string& content = message.content(); + ASSERT_EQ(0, static_cast<int>(content.size())); + } +} + +void DevToolsRemoteListenSocketTester::TestServerSend() { + loop_->PostTask(FROM_HERE, NewRunnableMethod( + this, &DevToolsRemoteListenSocketTester::SendFromTester)); + ASSERT_TRUE(NextAction(TestTimeouts::action_timeout_ms())); + ASSERT_EQ(ACTION_SEND, last_action_.type()); + // TODO(erikkay): Without this sleep, the recv seems to fail a small amount + // of the time. I could fix this by making the socket blocking, but then + // this test might hang in the case of errors. It would be nice to do + // something that felt more reliable here. + base::PlatformThread::Sleep(10); // sleep for 10ms + const int buf_len = 200; + char buf[buf_len+1]; + int recv_len = HANDLE_EINTR(recv(test_socket_, buf, buf_len, 0)); + ASSERT_NE(recv_len, SOCKET_ERROR); + buf[recv_len] = 0; + ASSERT_STREQ(buf, kChromeDevToolsHandshake); +} + +DevToolsRemoteListenSocketTester::~DevToolsRemoteListenSocketTester() {} + +class DevToolsRemoteListenSocketTest: public PlatformTest { + public: + DevToolsRemoteListenSocketTest() { + tester_ = NULL; + } + + virtual void SetUp() { + PlatformTest::SetUp(); + tester_ = new DevToolsRemoteListenSocketTester(); + tester_->SetUp(); + } + + virtual void TearDown() { + PlatformTest::TearDown(); + tester_->TearDown(); + tester_ = NULL; + } + + scoped_refptr<DevToolsRemoteListenSocketTester> tester_; +}; + +// This test is flaky; see comment in ::TestServerSend. +TEST_F(DevToolsRemoteListenSocketTest, ServerSend) { + tester_->TestServerSend(); +} + +TEST_F(DevToolsRemoteListenSocketTest, ClientSend) { + tester_->TestClientSend(); +} diff --git a/chrome/browser/debugger/devtools_remote_listen_socket_unittest.h b/chrome/browser/debugger/devtools_remote_listen_socket_unittest.h new file mode 100644 index 0000000..0b9b36b --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_listen_socket_unittest.h @@ -0,0 +1,129 @@ +// Copyright (c) 2011 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_DEVTOOLS_REMOTE_LISTEN_SOCKET_UNITTEST_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_LISTEN_SOCKET_UNITTEST_H_ +#pragma once + +#include "build/build_config.h" + +#include <deque> +#include <string> + +#if defined(OS_WIN) +#include <winsock2.h> +#elif defined(OS_POSIX) +#include <sys/socket.h> +#include <errno.h> +#include <semaphore.h> +#include <arpa/inet.h> +#endif + +#include "base/threading/thread.h" +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/string_util.h" +#include "base/threading/thread.h" +#include "chrome/browser/debugger/devtools_remote.h" +#include "chrome/browser/debugger/devtools_remote_listen_socket.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "net/base/net_util.h" +#include "net/base/listen_socket.h" +#include "net/base/winsock_init.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_POSIX) +// Used same name as in Windows to avoid #ifdef where refrenced +#define SOCKET int +const int INVALID_SOCKET = -1; +const int SOCKET_ERROR = -1; +#endif + +enum ActionType { + ACTION_NONE = 0, + ACTION_LISTEN = 1, + ACTION_ACCEPT = 2, + ACTION_READ = 3, + ACTION_READ_MESSAGE = 4, + ACTION_SEND = 5, + ACTION_CLOSE = 6, + ACTION_SHUTDOWN = 7 +}; + +class ListenSocketTestAction { + public: + ListenSocketTestAction(); + explicit ListenSocketTestAction(ActionType action); + ListenSocketTestAction(ActionType action, std::string data); + ListenSocketTestAction(ActionType action, + const DevToolsRemoteMessage& message); + ~ListenSocketTestAction(); + + const std::string data() const { return data_; } + const DevToolsRemoteMessage message() { return message_; } + ActionType type() const { return action_; } + + private: + ActionType action_; + std::string data_; + DevToolsRemoteMessage message_; +}; + + +// This had to be split out into a separate class because I couldn't +// make a the testing::Test class refcounted. +class DevToolsRemoteListenSocketTester : + public DevToolsRemoteListener { + public: + DevToolsRemoteListenSocketTester(); + + virtual void SetUp(); + virtual void TearDown(); + + void ReportAction(const ListenSocketTestAction& action); + bool NextAction(int timeout); + + // DevToolsRemoteMessageHandler interface + virtual void HandleMessage(const DevToolsRemoteMessage& message); + virtual void OnAcceptConnection(net::ListenSocket* connection); + virtual void OnConnectionLost(); + + // read all pending data from the test socket + int ClearTestSocket(); + // Release the connection and server sockets + void Shutdown(); + void Listen(); + void SendFromTester(); + virtual bool Send(SOCKET sock, const std::string& str); + // verify the send/read from client to server + void TestClientSend(); + // verify a send/read from server to client + void TestServerSend(); + +#if defined(OS_WIN) + CRITICAL_SECTION lock_; + HANDLE semaphore_; +#elif defined(OS_POSIX) + pthread_mutex_t lock_; + sem_t* semaphore_; +#endif + + scoped_ptr<base::Thread> thread_; + MessageLoopForIO* loop_; + net::ListenSocket* server_; + net::ListenSocket* connection_; + ListenSocketTestAction last_action_; + std::deque<ListenSocketTestAction> queue_; + SOCKET test_socket_; + static const int kTestPort; + + protected: + virtual net::ListenSocket* DoListen(); + + private: + virtual ~DevToolsRemoteListenSocketTester(); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_LISTEN_SOCKET_UNITTEST_H_ diff --git a/chrome/browser/debugger/devtools_remote_message.cc b/chrome/browser/debugger/devtools_remote_message.cc new file mode 100644 index 0000000..c64a027 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_message.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2011 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/devtools_remote_message.h" + +#include "base/string_number_conversions.h" + +const char DevToolsRemoteMessageHeaders::kContentLength[] = "Content-Length"; +const char DevToolsRemoteMessageHeaders::kTool[] = "Tool"; +const char DevToolsRemoteMessageHeaders::kDestination[] = "Destination"; + +const char DevToolsRemoteMessage::kEmptyValue[] = ""; + +DevToolsRemoteMessageBuilder& DevToolsRemoteMessageBuilder::instance() { + static DevToolsRemoteMessageBuilder instance_; + return instance_; +} + +DevToolsRemoteMessage::DevToolsRemoteMessage() {} + +DevToolsRemoteMessage::DevToolsRemoteMessage(const HeaderMap& headers, + const std::string& content) + : header_map_(headers), + content_(content) { +} + +DevToolsRemoteMessage::~DevToolsRemoteMessage() {} + +const std::string DevToolsRemoteMessage::GetHeader( + const std::string& header_name, + const std::string& default_value) const { + HeaderMap::const_iterator it = header_map_.find(header_name); + if (it == header_map_.end()) { + return default_value; + } + return it->second; +} + +const std::string DevToolsRemoteMessage::GetHeaderWithEmptyDefault( + const std::string& header_name) const { + return GetHeader(header_name, DevToolsRemoteMessage::kEmptyValue); +} + +const std::string DevToolsRemoteMessage::ToString() const { + std::string result; + for (HeaderMap::const_iterator it = header_map_.begin(), + end = header_map_.end(); it != end; ++it) { + result.append(it->first).append(":").append(it->second).append("\r\n"); + } + result.append("\r\n").append(content_); + return result; +} + +DevToolsRemoteMessage* DevToolsRemoteMessageBuilder::Create( + const std::string& tool, + const std::string& destination, + const std::string& content) { + DevToolsRemoteMessage::HeaderMap headers; + headers[DevToolsRemoteMessageHeaders::kContentLength] = + base::IntToString(content.size()); + headers[DevToolsRemoteMessageHeaders::kTool] = tool; + headers[DevToolsRemoteMessageHeaders::kDestination] = destination; + return new DevToolsRemoteMessage(headers, content); +} diff --git a/chrome/browser/debugger/devtools_remote_message.h b/chrome/browser/debugger/devtools_remote_message.h new file mode 100644 index 0000000..5e3197c --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_message.h @@ -0,0 +1,99 @@ +// Copyright (c) 2011 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_DEVTOOLS_REMOTE_MESSAGE_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_MESSAGE_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/hash_tables.h" + +// Contains DevTools protocol message header names +// and the Flags header bit field constants. +struct DevToolsRemoteMessageHeaders { + // The content length in decimal. + static const char kContentLength[]; + // The tool that should handle the message. + static const char kTool[]; + // The destination (inspected) object identifier (if any), like a TabID. + static const char kDestination[]; +}; + +// Represents a Chrome remote debugging protocol message transferred +// over the wire between the remote debugger and a Chrome instance. +// Consider using DevToolsRemoteMessageBuilder (see end of this file) for easy +// construction of outbound (Chrome -> remote debugger) messages. +class DevToolsRemoteMessage { + public: + typedef base::hash_map<std::string, std::string> HeaderMap; + + // Use this as the second parameter in a |GetHeader| call to use + // an empty string as the default value. + static const char kEmptyValue[]; + + // Constructs an empty message with no content or headers. + DevToolsRemoteMessage(); + DevToolsRemoteMessage(const HeaderMap& headers, const std::string& content); + virtual ~DevToolsRemoteMessage(); + + const HeaderMap& headers() const { + return header_map_; + } + + const std::string& content() const { + return content_; + } + + int content_length() const { + return content_.size(); + } + + const std::string tool() const { + return GetHeaderWithEmptyDefault(DevToolsRemoteMessageHeaders::kTool); + } + + const std::string destination() const { + return GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kDestination); + } + + // Returns the header value providing default_value if the header is absent. + const std::string GetHeader(const std::string& header_name, + const std::string& default_value) const; + + // Returns the header value providing an empty string if the header is absent. + const std::string GetHeaderWithEmptyDefault( + const std::string& header_name) const; + + // Returns a string representation of the message useful for the transfer to + // the remote debugger. + const std::string ToString() const; + + private: + HeaderMap header_map_; + std::string content_; + // Cannot DISALLOW_COPY_AND_ASSIGN(DevToolsRemoteMessage) since it is passed + // as an IPC message argument and needs to be copied. +}; + +// Facilitates easy construction of outbound (Chrome -> remote debugger) +// DevToolsRemote messages. +class DevToolsRemoteMessageBuilder { + public: + // A singleton instance getter. + static DevToolsRemoteMessageBuilder& instance(); + // Creates a message given the certain header values and a payload. + DevToolsRemoteMessage* Create(const std::string& tool, + const std::string& destination, + const std::string& payload); + + private: + DevToolsRemoteMessageBuilder() {} + virtual ~DevToolsRemoteMessageBuilder() {} + DISALLOW_COPY_AND_ASSIGN(DevToolsRemoteMessageBuilder); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_MESSAGE_H_ diff --git a/chrome/browser/debugger/devtools_remote_message_unittest.cc b/chrome/browser/debugger/devtools_remote_message_unittest.cc new file mode 100644 index 0000000..670cd79 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_message_unittest.cc @@ -0,0 +1,70 @@ +// Copyright (c) 2011 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 <string> + +#include "base/memory/scoped_ptr.h" +#include "base/string_number_conversions.h" +#include "chrome/browser/debugger/devtools_remote.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "testing/gtest/include/gtest/gtest.h" + +class DevToolsRemoteMessageTest : public testing::Test { + public: + DevToolsRemoteMessageTest() : testing::Test() {} + + protected: + virtual void SetUp() { + testing::Test::SetUp(); + } +}; + +TEST_F(DevToolsRemoteMessageTest, ConstructInstanceManually) { + DevToolsRemoteMessage::HeaderMap headers; + std::string content = "{\"command\":\"ping\"}"; + headers[DevToolsRemoteMessageHeaders::kTool] = "DevToolsService"; + headers[DevToolsRemoteMessageHeaders::kContentLength] = + base::IntToString(content.size()); + + DevToolsRemoteMessage message(headers, content); + ASSERT_STREQ("DevToolsService", + message.GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kTool).c_str()); + ASSERT_STREQ("DevToolsService", message.tool().c_str()); + ASSERT_STREQ(content.c_str(), message.content().c_str()); + ASSERT_EQ(content.size(), + static_cast<std::string::size_type>(message.content_length())); + ASSERT_EQ(static_cast<DevToolsRemoteMessage::HeaderMap::size_type>(2), + message.headers().size()); +} + +TEST_F(DevToolsRemoteMessageTest, ConstructWithBuilder) { + std::string content = "Responsecontent"; + scoped_ptr<DevToolsRemoteMessage> message( + DevToolsRemoteMessageBuilder::instance().Create( + "V8Debugger", // tool + "2", // destination + content)); // content + + ASSERT_EQ(static_cast<DevToolsRemoteMessage::HeaderMap::size_type>(3), + message->headers().size()); + ASSERT_STREQ( + "V8Debugger", + message->GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kTool).c_str()); + ASSERT_STREQ( + "V8Debugger", + message->tool().c_str()); + ASSERT_STREQ( + "2", + message->GetHeaderWithEmptyDefault( + DevToolsRemoteMessageHeaders::kDestination).c_str()); + ASSERT_STREQ( + "2", + message->destination().c_str()); + ASSERT_EQ(content.size(), + static_cast<DevToolsRemoteMessage::HeaderMap::size_type>( + message->content_length())); + ASSERT_STREQ(content.c_str(), message->content().c_str()); +} diff --git a/chrome/browser/debugger/devtools_remote_service.cc b/chrome/browser/debugger/devtools_remote_service.cc new file mode 100644 index 0000000..329e1a8 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_service.cc @@ -0,0 +1,110 @@ +// Copyright (c) 2011 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/devtools_remote_service.h" + +#include <string> + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "chrome/browser/debugger/devtools_protocol_handler.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "chrome/browser/debugger/inspectable_tab_proxy.h" +#include "chrome/browser/sessions/restore_tab_helper.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "content/browser/debugger/devtools_manager.h" +#include "content/browser/tab_contents/navigation_controller.h" +#include "content/browser/tab_contents/navigation_entry.h" +#include "content/common/devtools_messages.h" + +const char DevToolsRemoteServiceCommand::kPing[] = "ping"; +const char DevToolsRemoteServiceCommand::kVersion[] = "version"; +const char DevToolsRemoteServiceCommand::kListTabs[] = "list_tabs"; + +const char DevToolsRemoteService::kToolName[] = "DevToolsService"; + +namespace { +const char kCommandKey[] = "command"; +const char kDataKey[] = "data"; +const char kResultKey[] = "result"; +} // namespace + +DevToolsRemoteService::DevToolsRemoteService(DevToolsProtocolHandler* delegate) + : delegate_(delegate) {} + +DevToolsRemoteService::~DevToolsRemoteService() {} + +void DevToolsRemoteService::HandleMessage( + const DevToolsRemoteMessage& message) { + scoped_ptr<Value> request(base::JSONReader::Read(message.content(), false)); + if (request.get() == NULL) { + // Bad JSON + NOTREACHED(); + return; + } + DictionaryValue* json; + if (request->IsType(Value::TYPE_DICTIONARY)) { + json = static_cast<DictionaryValue*>(request.get()); + if (!json->HasKey(kCommandKey)) { + NOTREACHED(); // Broken protocol - no "command" specified + return; + } + } else { + NOTREACHED(); // Broken protocol - not a JS object + return; + } + ProcessJson(json, message); +} + +void DevToolsRemoteService::ProcessJson(DictionaryValue* json, + const DevToolsRemoteMessage& message) { + static const std::string kOkResponse = "ok"; // "Ping" response + static const std::string kVersion = "0.1"; // Current protocol version + std::string command; + DictionaryValue response; + + json->GetString(kCommandKey, &command); + response.SetString(kCommandKey, command); + + if (command == DevToolsRemoteServiceCommand::kPing) { + response.SetInteger(kResultKey, Result::kOk); + response.SetString(kDataKey, kOkResponse); + } else if (command == DevToolsRemoteServiceCommand::kVersion) { + response.SetInteger(kResultKey, Result::kOk); + response.SetString(kDataKey, kVersion); + } else if (command == DevToolsRemoteServiceCommand::kListTabs) { + ListValue* data = new ListValue(); + const InspectableTabProxy::TabMap& tab_map = + delegate_->inspectable_tab_proxy()->tab_map(); + for (InspectableTabProxy::TabMap::const_iterator it = + tab_map.begin(), end = tab_map.end(); it != end; ++it) { + NavigationEntry* entry = it->second->controller().GetActiveEntry(); + if (entry == NULL) { + continue; + } + if (entry->url().is_valid()) { + ListValue* tab = new ListValue(); + tab->Append(Value::CreateIntegerValue( + it->second->restore_tab_helper()->session_id().id())); + tab->Append(Value::CreateStringValue(entry->url().spec())); + data->Append(tab); + } + } + response.SetInteger(kResultKey, Result::kOk); + response.Set(kDataKey, data); + } else { + // Unknown protocol command. + NOTREACHED(); + response.SetInteger(kResultKey, Result::kUnknownCommand); + } + std::string response_json; + base::JSONWriter::Write(&response, false, &response_json); + scoped_ptr<DevToolsRemoteMessage> response_message( + DevToolsRemoteMessageBuilder::instance().Create(message.tool(), + message.destination(), + response_json)); + delegate_->Send(*response_message.get()); +} diff --git a/chrome/browser/debugger/devtools_remote_service.h b/chrome/browser/debugger/devtools_remote_service.h new file mode 100644 index 0000000..c06ccf5 --- /dev/null +++ b/chrome/browser/debugger/devtools_remote_service.h @@ -0,0 +1,50 @@ +// Copyright (c) 2011 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_DEVTOOLS_REMOTE_SERVICE_H_ +#define CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_SERVICE_H_ +#pragma once + +#include "base/basictypes.h" +#include "chrome/browser/debugger/devtools_remote.h" + +class DevToolsRemoteMessage; +class DevToolsProtocolHandler; + +namespace base { +class DictionaryValue; +} + +// Contains constants for DevToolsRemoteService tool protocol commands. +struct DevToolsRemoteServiceCommand { + static const char kPing[]; + static const char kVersion[]; + static const char kListTabs[]; +}; + +// Handles Chrome remote debugger protocol service commands. +class DevToolsRemoteService : public DevToolsRemoteListener { + public: + explicit DevToolsRemoteService(DevToolsProtocolHandler* delegate); + + // DevToolsRemoteListener interface + virtual void HandleMessage(const DevToolsRemoteMessage& message); + virtual void OnConnectionLost() {} + + static const char kToolName[]; + + private: + // Operation result returned in the "result" field. + struct Result { + static const int kOk = 0; + static const int kUnknownCommand = 1; + }; + virtual ~DevToolsRemoteService(); + void ProcessJson(base::DictionaryValue* json, + const DevToolsRemoteMessage& message); + DevToolsProtocolHandler* delegate_; + DISALLOW_COPY_AND_ASSIGN(DevToolsRemoteService); +}; + +#endif // CHROME_BROWSER_DEBUGGER_DEVTOOLS_REMOTE_SERVICE_H_ diff --git a/chrome/browser/debugger/devtools_sanity_unittest.cc b/chrome/browser/debugger/devtools_sanity_unittest.cc new file mode 100644 index 0000000..37bd337 --- /dev/null +++ b/chrome/browser/debugger/devtools_sanity_unittest.cc @@ -0,0 +1,443 @@ +// Copyright (c) 2011 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 "base/command_line.h" +#include "base/path_service.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/debugger/devtools_window.h" +#include "chrome/browser/extensions/extension_host.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/common/chrome_notification_types.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/in_process_browser_test.h" +#include "chrome/test/ui_test_utils.h" +#include "content/browser/content_browser_client.h" +#include "content/browser/debugger/devtools_client_host.h" +#include "content/browser/debugger/devtools_manager.h" +#include "content/browser/debugger/worker_devtools_manager_io.h" +#include "content/browser/renderer_host/render_view_host.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/browser/worker_host/worker_process_host.h" +#include "content/common/notification_registrar.h" +#include "content/common/notification_service.h" +#include "net/test/test_server.h" + +namespace { + +// Used to block until a dev tools client window's browser is closed. +class BrowserClosedObserver : public NotificationObserver { + public: + explicit BrowserClosedObserver(Browser* browser) { + registrar_.Add(this, chrome::NOTIFICATION_BROWSER_CLOSED, + Source<Browser>(browser)); + ui_test_utils::RunMessageLoop(); + } + + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) { + MessageLoopForUI::current()->Quit(); + } + + private: + NotificationRegistrar registrar_; + DISALLOW_COPY_AND_ASSIGN(BrowserClosedObserver); +}; + +// The delay waited in some cases where we don't have a notifications for an +// action we take. +const int kActionDelayMs = 500; + +const char kDebuggerTestPage[] = "files/devtools/debugger_test_page.html"; +const char kPauseWhenLoadingDevTools[] = + "files/devtools/pause_when_loading_devtools.html"; +const char kPauseWhenScriptIsRunning[] = + "files/devtools/pause_when_script_is_running.html"; +const char kPageWithContentScript[] = + "files/devtools/page_with_content_script.html"; +const char kChunkedTestPage[] = "chunked"; +const char kSlowTestPage[] = + "chunked?waitBeforeHeaders=100&waitBetweenChunks=100&chunksNumber=2"; +const char kSharedWorkerTestPage[] = + "files/workers/workers_ui_shared_worker.html"; + +void RunTestFuntion(DevToolsWindow* window, const char* test_name) { + std::string result; + + // At first check that JavaScript part of the front-end is loaded by + // checking that global variable uiTests exists(it's created after all js + // files have been loaded) and has runTest method. + ASSERT_TRUE( + ui_test_utils::ExecuteJavaScriptAndExtractString( + window->GetRenderViewHost(), + L"", + L"window.domAutomationController.send(" + L"'' + (window.uiTests && (typeof uiTests.runTest)));", + &result)); + + if (result == "function") { + ASSERT_TRUE( + ui_test_utils::ExecuteJavaScriptAndExtractString( + window->GetRenderViewHost(), + L"", + UTF8ToWide(base::StringPrintf("uiTests.runTest('%s')", + test_name)), + &result)); + EXPECT_EQ("[OK]", result); + } else { + FAIL() << "DevTools front-end is broken."; + } +} + +class DevToolsSanityTest : public InProcessBrowserTest { + public: + DevToolsSanityTest() + : window_(NULL), + inspected_rvh_(NULL) { + set_show_window(true); + EnableDOMAutomation(); + } + + protected: + void RunTest(const std::string& test_name, const std::string& test_page) { + OpenDevToolsWindow(test_page); + RunTestFuntion(window_, test_name.c_str()); + CloseDevToolsWindow(); + } + + void OpenDevToolsWindow(const std::string& test_page) { + ASSERT_TRUE(test_server()->Start()); + GURL url = test_server()->GetURL(test_page); + ui_test_utils::NavigateToURL(browser(), url); + + inspected_rvh_ = GetInspectedTab()->render_view_host(); + window_ = DevToolsWindow::OpenDevToolsWindow(inspected_rvh_); + RenderViewHost* client_rvh = window_->GetRenderViewHost(); + TabContents* client_contents = client_rvh->delegate()->GetAsTabContents(); + ui_test_utils::WaitForNavigation(&client_contents->controller()); + } + + TabContents* GetInspectedTab() { + return browser()->GetTabContentsAt(0); + } + + void CloseDevToolsWindow() { + DevToolsManager* devtools_manager = DevToolsManager::GetInstance(); + // UnregisterDevToolsClientHostFor may destroy window_ so store the browser + // first. + Browser* browser = window_->browser(); + devtools_manager->UnregisterDevToolsClientHostFor(inspected_rvh_); + + // Wait only when DevToolsWindow has a browser. For docked DevTools, this + // is NULL and we skip the wait. + if (browser) + BrowserClosedObserver close_observer(browser); + } + + DevToolsWindow* window_; + RenderViewHost* inspected_rvh_; +}; + + +class CancelableQuitTask : public Task { + public: + explicit CancelableQuitTask(const std::string& timeout_message) + : timeout_message_(timeout_message), + cancelled_(false) { + } + + void cancel() { + cancelled_ = true; + } + + virtual void Run() { + if (cancelled_) { + return; + } + FAIL() << timeout_message_; + MessageLoop::current()->Quit(); + } + + private: + std::string timeout_message_; + bool cancelled_; +}; + + +// Base class for DevTools tests that test devtools functionality for +// extensions and content scripts. +class DevToolsExtensionDebugTest : public DevToolsSanityTest, + public NotificationObserver { + public: + DevToolsExtensionDebugTest() : DevToolsSanityTest() { + PathService::Get(chrome::DIR_TEST_DATA, &test_extensions_dir_); + test_extensions_dir_ = test_extensions_dir_.AppendASCII("devtools"); + test_extensions_dir_ = test_extensions_dir_.AppendASCII("extensions"); + } + + protected: + // Load an extention from test\data\devtools\extensions\<extension_name> + void LoadExtension(const char* extension_name) { + FilePath path = test_extensions_dir_.AppendASCII(extension_name); + ASSERT_TRUE(LoadExtensionFromPath(path)) << "Failed to load extension."; + } + + private: + bool LoadExtensionFromPath(const FilePath& path) { + ExtensionService* service = browser()->profile()->GetExtensionService(); + size_t num_before = service->extensions()->size(); + { + NotificationRegistrar registrar; + registrar.Add(this, chrome::NOTIFICATION_EXTENSION_LOADED, + NotificationService::AllSources()); + CancelableQuitTask* delayed_quit = + new CancelableQuitTask("Extension load timed out."); + MessageLoop::current()->PostDelayedTask(FROM_HERE, delayed_quit, + 4*1000); + service->LoadExtension(path); + ui_test_utils::RunMessageLoop(); + delayed_quit->cancel(); + } + size_t num_after = service->extensions()->size(); + if (num_after != (num_before + 1)) + return false; + + return WaitForExtensionHostsToLoad(); + } + + bool WaitForExtensionHostsToLoad() { + // Wait for all the extension hosts that exist to finish loading. + // NOTE: This assumes that the extension host list is not changing while + // this method is running. + + NotificationRegistrar registrar; + registrar.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING, + NotificationService::AllSources()); + CancelableQuitTask* delayed_quit = + new CancelableQuitTask("Extension host load timed out."); + MessageLoop::current()->PostDelayedTask(FROM_HERE, delayed_quit, + 4*1000); + + ExtensionProcessManager* manager = + browser()->profile()->GetExtensionProcessManager(); + for (ExtensionProcessManager::const_iterator iter = manager->begin(); + iter != manager->end();) { + if ((*iter)->did_stop_loading()) + ++iter; + else + ui_test_utils::RunMessageLoop(); + } + + delayed_quit->cancel(); + return true; + } + + void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) { + switch (type) { + case chrome::NOTIFICATION_EXTENSION_LOADED: + case chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: + MessageLoopForUI::current()->Quit(); + break; + default: + NOTREACHED(); + break; + } + } + + FilePath test_extensions_dir_; +}; + + + +// Used to block until a navigation completes. +class LoadStopObserver : public NotificationObserver { + public: + explicit LoadStopObserver(const NotificationSource& source) : done_(false) { + registrar_.Add(this, content::NOTIFICATION_LOAD_STOP, source); + ui_test_utils::RunMessageLoop(); + } + + private: + virtual void Observe(int type, + const NotificationSource& source, + const NotificationDetails& details) { + if (type == content::NOTIFICATION_LOAD_STOP) { + if (done_) + return; + done_ = true; + MessageLoopForUI::current()->Quit(); + } + } + + NotificationRegistrar registrar_; + bool done_; + DISALLOW_COPY_AND_ASSIGN(LoadStopObserver); +}; + + +class WorkerDevToolsSanityTest : public InProcessBrowserTest { + public: + WorkerDevToolsSanityTest() : window_(NULL) { + set_show_window(true); + EnableDOMAutomation(); + } + + protected: + void RunTest(const char* test_name, const char* test_page) { + ASSERT_TRUE(test_server()->Start()); + GURL url = test_server()->GetURL(test_page); + ui_test_utils::NavigateToURL(browser(), url); + + OpenDevToolsWindowForFirstSharedWorker(); + RunTestFuntion(window_, test_name); + CloseDevToolsWindow(); + } + + static void OpenDevToolsWindowForFirstSharedWorkerOnIOThread(int attempt) { + BrowserChildProcessHost::Iterator iter(ChildProcessInfo::WORKER_PROCESS); + bool found = false; + for (; !iter.Done(); ++iter) { + WorkerProcessHost* worker = static_cast<WorkerProcessHost*>(*iter); + const WorkerProcessHost::Instances& instances = worker->instances(); + for (WorkerProcessHost::Instances::const_iterator i = instances.begin(); + i != instances.end(); ++i) { + if (!i->shared()) + continue; + WorkerDevToolsManagerIO::GetInstance()->OpenDevToolsForWorker( + worker->id(), i->worker_route_id()); + found = true; + break; + } + } + if (found) { + BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, + new MessageLoop::QuitTask); + } else if (attempt < 30) { + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + NewRunnableFunction( + &OpenDevToolsWindowForFirstSharedWorkerOnIOThread, attempt + 1), + 100); + } else { + FAIL() << "Shared worker not found."; + } + + } + + void OpenDevToolsWindowForFirstSharedWorker() { + BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, NewRunnableFunction( + &OpenDevToolsWindowForFirstSharedWorkerOnIOThread, 1)); + ui_test_utils::RunMessageLoop(); + window_ = static_cast<DevToolsWindow*>( + DevToolsClientHost::GetDevToolsClientHostForTest()); + ASSERT_TRUE(window_ != NULL); + + RenderViewHost* client_rvh = window_->GetRenderViewHost(); + TabContents* client_contents = client_rvh->delegate()->GetAsTabContents(); + if (client_contents->is_loading()) { + LoadStopObserver( + Source<NavigationController>(&client_contents->controller())); + } + } + + void CloseDevToolsWindow() { + Browser* browser = window_->browser(); + browser->CloseAllTabs(); + BrowserClosedObserver close_observer(browser); + } + + DevToolsWindow* window_; +}; + + +// Tests scripts panel showing. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestShowScriptsTab) { + RunTest("testShowScriptsTab", kDebuggerTestPage); +} + +// Tests that scripts tab is populated with inspected scripts even if it +// hadn't been shown by the moment inspected paged refreshed. +// @see http://crbug.com/26312 +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, + TestScriptsTabIsPopulatedOnInspectedPageRefresh) { + // Clear inspector settings to ensure that Elements will be + // current panel when DevTools window is open. + content::GetContentClient()->browser()->ClearInspectorSettings( + GetInspectedTab()->render_view_host()); + RunTest("testScriptsTabIsPopulatedOnInspectedPageRefresh", + kDebuggerTestPage); +} + +// Tests that a content script is in the scripts list. +// This test is disabled, see bug 28961. +IN_PROC_BROWSER_TEST_F(DevToolsExtensionDebugTest, + TestContentScriptIsPresent) { + LoadExtension("simple_content_script"); + RunTest("testContentScriptIsPresent", kPageWithContentScript); +} + +// Tests that scripts are not duplicated after Scripts Panel switch. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, + TestNoScriptDuplicatesOnPanelSwitch) { + RunTest("testNoScriptDuplicatesOnPanelSwitch", kDebuggerTestPage); +} + +// Tests that debugger works correctly if pause event occurs when DevTools +// frontend is being loaded. +// Flaky - http://crbug.com/69719. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, FLAKY_TestPauseWhenLoadingDevTools) { + RunTest("testPauseWhenLoadingDevTools", kPauseWhenLoadingDevTools); +} + +// Tests that pressing 'Pause' will pause script execution if the script +// is already running. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, FLAKY_TestPauseWhenScriptIsRunning) { + RunTest("testPauseWhenScriptIsRunning", kPauseWhenScriptIsRunning); +} + +// Tests network timing. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestNetworkTiming) { + RunTest("testNetworkTiming", kSlowTestPage); +} + +// Tests network size. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestNetworkSize) { + RunTest("testNetworkSize", kChunkedTestPage); +} + +// Tests raw headers text. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestNetworkSyncSize) { + RunTest("testNetworkSyncSize", kChunkedTestPage); +} + +// Tests raw headers text. +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestNetworkRawHeadersText) { + RunTest("testNetworkRawHeadersText", kChunkedTestPage); +} + +IN_PROC_BROWSER_TEST_F(DevToolsSanityTest, TestPageWithNoJavaScript) { + OpenDevToolsWindow("about:blank"); + std::string result; + ASSERT_TRUE( + ui_test_utils::ExecuteJavaScriptAndExtractString( + window_->GetRenderViewHost(), + L"", + L"window.domAutomationController.send(" + L"'' + (window.uiTests && (typeof uiTests.runTest)));", + &result)); + ASSERT_EQ("function", result) << "DevTools front-end is broken."; + CloseDevToolsWindow(); +} + +// http://crbug.com/89845 +IN_PROC_BROWSER_TEST_F(WorkerDevToolsSanityTest, DISABLED_InspectSharedWorker) { + RunTest("testSharedWorker", kSharedWorkerTestPage); +} + +} // namespace diff --git a/chrome/browser/debugger/devtools_window.cc b/chrome/browser/debugger/devtools_window.cc index b5161da..aa697cd 100644 --- a/chrome/browser/debugger/devtools_window.cc +++ b/chrome/browser/debugger/devtools_window.cc @@ -237,7 +237,7 @@ void DevToolsWindow::Show(DevToolsToggleAction action) { ScheduleAction(action); } -void DevToolsWindow::Activate() { +void DevToolsWindow::RequestActivate() { if (!docked_) { if (!browser_->window()->IsActive()) { browser_->window()->Activate(); @@ -249,7 +249,7 @@ void DevToolsWindow::Activate() { } } -void DevToolsWindow::SetDocked(bool docked) { +void DevToolsWindow::RequestSetDocked(bool docked) { if (docked_ == docked) return; @@ -283,14 +283,14 @@ void DevToolsWindow::SetDocked(bool docked) { Show(DEVTOOLS_TOGGLE_ACTION_NONE); } -void DevToolsWindow::Close() { +void DevToolsWindow::RequestClose() { DCHECK(docked_); NotifyCloseListener(); InspectedTabClosing(); } -void DevToolsWindow::SaveAs(const std::string& suggested_file_name, - const std::string& content) { +void DevToolsWindow::RequestSaveAs(const std::string& suggested_file_name, + const std::string& content) { DevToolsFileUtil::SaveAs(tab_contents_->profile(), suggested_file_name, content); diff --git a/chrome/browser/debugger/devtools_window.h b/chrome/browser/debugger/devtools_window.h index 0692e85..3d1e706 100644 --- a/chrome/browser/debugger/devtools_window.h +++ b/chrome/browser/debugger/devtools_window.h @@ -54,11 +54,11 @@ class DevToolsWindow virtual void InspectedTabClosing(); virtual void TabReplaced(TabContents* new_tab); virtual RenderViewHost* GetClientRenderViewHost(); - virtual void Activate(); - virtual void SetDocked(bool docked); - virtual void Close(); - virtual void SaveAs(const std::string& suggested_file_name, - const std::string& content); + virtual void RequestActivate(); + virtual void RequestSetDocked(bool docked); + virtual void RequestClose(); + virtual void RequestSaveAs(const std::string& suggested_file_name, + const std::string& content); RenderViewHost* GetRenderViewHost(); void Show(DevToolsToggleAction action); diff --git a/chrome/browser/debugger/extension_ports_remote_service.cc b/chrome/browser/debugger/extension_ports_remote_service.cc new file mode 100644 index 0000000..3c0275d --- /dev/null +++ b/chrome/browser/debugger/extension_ports_remote_service.cc @@ -0,0 +1,385 @@ +// Copyright (c) 2011 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. + +// Implementation of the ExtensionPortsRemoteService. + +// Inspired significantly from debugger_remote_service +// and ../automation/extension_port_container. + +#include "chrome/browser/debugger/extension_ports_remote_service.h" + +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/message_loop.h" +#include "base/string_number_conversions.h" +#include "base/values.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/debugger/devtools_protocol_handler.h" +#include "chrome/browser/debugger/devtools_remote_message.h" +#include "chrome/browser/debugger/inspectable_tab_proxy.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "chrome/common/extensions/extension_messages.h" +#include "content/browser/debugger/devtools_manager.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/common/devtools_messages.h" + +namespace { + +// Protocol is as follows: +// +// From external client: +// {"command": "connect", +// "data": { +// "extensionId": "<extension_id string>", +// "channelName": "<port name string>", (optional) +// "tabId": <numerical tab ID> (optional) +// } +// } +// To connect to a background page or tool strip, the tabId should be omitted. +// Tab IDs can be enumerated with the list_tabs DevToolsService command. +// +// Response: +// {"command": "connect", +// "result": 0, (assuming success) +// "data": { +// "portId": <numerical port ID> +// } +// } +// +// Posting a message from external client: +// Put the target message port ID in the devtools destination field. +// {"command": "postMessage", +// "data": <message body - arbitrary JSON> +// } +// Response: +// {"command": "postMessage", +// "result": 0 (Assuming success) +// } +// Note this is a confirmation from the devtools protocol layer, not +// a response from the extension. +// +// Message from an extension to the external client: +// The message port ID is in the devtools destination field. +// {"command": "onMessage", +// "result": 0, (Always 0) +// "data": <message body - arbitrary JSON> +// } +// +// The "disconnect" command from the external client, and +// "onDisconnect" notification from the ExtensionMessageService, are +// similar: with the message port ID in the destination field, but no +// "data" field in this case. + +// Commands: +const char kConnect[] = "connect"; +const char kDisconnect[] = "disconnect"; +const char kPostMessage[] = "postMessage"; +// Events: +const char kOnMessage[] = "onMessage"; +const char kOnDisconnect[] = "onDisconnect"; + +// Constants for the JSON message fields. +// The type is wstring because the constant is used to get a +// DictionaryValue field (which requires a wide string). + +// Mandatory. +const char kCommandKey[] = "command"; + +// Always present in messages sent to the external client. +const char kResultKey[] = "result"; + +// Field for command-specific parameters. Not strictly necessary, but +// makes it more similar to the remote debugger protocol, which should +// allow easier reuse of client code. +const char kDataKey[] = "data"; + +// Fields within the "data" dictionary: + +// Required for "connect": +const char kExtensionIdKey[] = "extensionId"; +// Optional in "connect": +const char kChannelNameKey[] = "channelName"; +const char kTabIdKey[] = "tabId"; + +// Present under "data" in replies to a successful "connect" . +const char kPortIdKey[] = "portId"; + +} // namespace + +const std::string ExtensionPortsRemoteService::kToolName = "ExtensionPorts"; + +ExtensionPortsRemoteService::ExtensionPortsRemoteService( + DevToolsProtocolHandler* delegate) + : delegate_(delegate), service_(NULL) { + // We need an ExtensionMessageService instance. It hangs off of + // |profile|. But we do not have a particular tab or RenderViewHost + // as context. I'll just use the first active profile not in + // incognito mode. But this is probably not the right way. + ProfileManager* profile_manager = g_browser_process->profile_manager(); + if (!profile_manager) { + LOG(WARNING) << "No profile manager for ExtensionPortsRemoteService"; + return; + } + + std::vector<Profile*> profiles(profile_manager->GetLoadedProfiles()); + for (size_t i = 0; i < profiles.size(); ++i) { + if (!profiles[i]->IsOffTheRecord()) { + service_ = profiles[i]->GetExtensionMessageService(); + break; + } + } + if (!service_) + LOG(WARNING) << "No usable profile for ExtensionPortsRemoteService"; +} + +ExtensionPortsRemoteService::~ExtensionPortsRemoteService() { +} + +void ExtensionPortsRemoteService::HandleMessage( + const DevToolsRemoteMessage& message) { + DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI); + const std::string destinationString = message.destination(); + scoped_ptr<Value> request(base::JSONReader::Read(message.content(), true)); + if (request.get() == NULL) { + // Bad JSON + NOTREACHED(); + return; + } + DictionaryValue* content; + if (!request->IsType(Value::TYPE_DICTIONARY)) { + NOTREACHED(); // Broken protocol :( + return; + } + content = static_cast<DictionaryValue*>(request.get()); + if (!content->HasKey(kCommandKey)) { + NOTREACHED(); // Broken protocol :( + return; + } + std::string command; + DictionaryValue response; + + content->GetString(kCommandKey, &command); + response.SetString(kCommandKey, command); + + if (!service_) { + // This happens if we failed to obtain an ExtensionMessageService + // during initialization. + NOTREACHED(); + response.SetInteger(kResultKey, RESULT_NO_SERVICE); + SendResponse(response, message.tool(), message.destination()); + return; + } + + int destination = -1; + if (!destinationString.empty()) + base::StringToInt(destinationString, &destination); + + if (command == kConnect) { + if (destination != -1) // destination should be empty for this command. + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + else + ConnectCommand(content, &response); + } else if (command == kDisconnect) { + if (destination == -1) // Destination required for this command. + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + else + DisconnectCommand(destination, &response); + } else if (command == kPostMessage) { + if (destination == -1) // Destination required for this command. + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + else + PostMessageCommand(destination, content, &response); + } else { + // Unknown command + NOTREACHED(); + response.SetInteger(kResultKey, RESULT_UNKNOWN_COMMAND); + } + SendResponse(response, message.tool(), message.destination()); +} + +void ExtensionPortsRemoteService::OnConnectionLost() { + VLOG(1) << "OnConnectionLost"; + DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI); + DCHECK(service_); + for (PortIdSet::iterator it = openPortIds_.begin(); + it != openPortIds_.end(); + ++it) + service_->CloseChannel(*it); + openPortIds_.clear(); +} + +void ExtensionPortsRemoteService::SendResponse( + const Value& response, const std::string& tool, + const std::string& destination) { + std::string response_content; + base::JSONWriter::Write(&response, false, &response_content); + scoped_ptr<DevToolsRemoteMessage> response_message( + DevToolsRemoteMessageBuilder::instance().Create( + tool, destination, response_content)); + delegate_->Send(*response_message.get()); +} + +bool ExtensionPortsRemoteService::Send(IPC::Message *message) { + DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI); + + IPC_BEGIN_MESSAGE_MAP(ExtensionPortsRemoteService, *message) + IPC_MESSAGE_HANDLER(ExtensionMsg_MessageInvoke, OnExtensionMessageInvoke) + IPC_MESSAGE_UNHANDLED_ERROR() + IPC_END_MESSAGE_MAP() + + delete message; + return true; +} + +void ExtensionPortsRemoteService::OnExtensionMessageInvoke( + const std::string& extension_id, + const std::string& function_name, + const ListValue& args, + const GURL& event_url) { + if (function_name == ExtensionMessageService::kDispatchOnMessage) { + DCHECK_EQ(args.GetSize(), 2u); + std::string message; + int port_id; + if (args.GetString(0, &message) && args.GetInteger(1, &port_id)) + OnExtensionMessage(message, port_id); + } else if (function_name == ExtensionMessageService::kDispatchOnDisconnect) { + DCHECK_EQ(args.GetSize(), 1u); + int port_id; + if (args.GetInteger(0, &port_id)) + OnExtensionPortDisconnected(port_id); + } else if (function_name == ExtensionMessageService::kDispatchOnConnect) { + // There is no way for this service to be addressed and receive + // connections. + NOTREACHED() << function_name << " shouldn't be called."; + } else { + NOTREACHED() << function_name << " shouldn't be called."; + } +} + +void ExtensionPortsRemoteService::OnExtensionMessage( + const std::string& message, int port_id) { + VLOG(1) << "Message event: from port " << port_id << ", < " << message << ">"; + // Transpose the information into a JSON message for the external client. + DictionaryValue content; + content.SetString(kCommandKey, kOnMessage); + content.SetInteger(kResultKey, RESULT_OK); + // Turn the stringified message body back into JSON. + Value* data = base::JSONReader::Read(message, false); + if (!data) { + NOTREACHED(); + return; + } + content.Set(kDataKey, data); + SendResponse(content, kToolName, base::IntToString(port_id)); +} + +void ExtensionPortsRemoteService::OnExtensionPortDisconnected(int port_id) { + VLOG(1) << "Disconnect event for port " << port_id; + openPortIds_.erase(port_id); + DictionaryValue content; + content.SetString(kCommandKey, kOnDisconnect); + content.SetInteger(kResultKey, RESULT_OK); + SendResponse(content, kToolName, base::IntToString(port_id)); +} + +void ExtensionPortsRemoteService::ConnectCommand( + DictionaryValue* content, DictionaryValue* response) { + // Parse out the parameters. + DictionaryValue* data; + if (!content->GetDictionary(kDataKey, &data)) { + response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR); + return; + } + std::string extension_id; + if (!data->GetString(kExtensionIdKey, &extension_id)) { + response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR); + return; + } + std::string channel_name = ""; + data->GetString(kChannelNameKey, &channel_name); // optional. + int tab_id = -1; + data->GetInteger(kTabIdKey, &tab_id); // optional. + int port_id; + if (tab_id != -1) { // Resolve the tab ID. + const InspectableTabProxy::TabMap& tab_map = + delegate_->inspectable_tab_proxy()->tab_map(); + InspectableTabProxy::TabMap::const_iterator it = tab_map.find(tab_id); + TabContents* tab_contents = NULL; + if (it != tab_map.end()) + tab_contents = it->second->tab_contents(); + if (!tab_contents) { + VLOG(1) << "tab not found: " << tab_id; + response->SetInteger(kResultKey, RESULT_TAB_NOT_FOUND); + return; + } + // Ask the ExtensionMessageService to open the channel. + VLOG(1) << "Connect: extension_id <" << extension_id + << ">, channel_name <" << channel_name + << ">, tab " << tab_id; + DCHECK(service_); + port_id = service_->OpenSpecialChannelToTab( + extension_id, channel_name, tab_contents, this); + } else { // no tab: channel to an extension' background page / toolstrip. + // Ask the ExtensionMessageService to open the channel. + VLOG(1) << "Connect: extension_id <" << extension_id + << ">, channel_name <" << channel_name << ">"; + DCHECK(service_); + port_id = service_->OpenSpecialChannelToExtension( + extension_id, channel_name, "null", this); + } + if (port_id == -1) { + // Failure: probably the extension ID doesn't exist. + VLOG(1) << "Connect failed"; + response->SetInteger(kResultKey, RESULT_CONNECT_FAILED); + return; + } + VLOG(1) << "Connected: port " << port_id; + openPortIds_.insert(port_id); + // Reply to external client with the port ID assigned to the new channel. + DictionaryValue* reply_data = new DictionaryValue(); + reply_data->SetInteger(kPortIdKey, port_id); + response->Set(kDataKey, reply_data); + response->SetInteger(kResultKey, RESULT_OK); +} + +void ExtensionPortsRemoteService::DisconnectCommand( + int port_id, DictionaryValue* response) { + VLOG(1) << "Disconnect port " << port_id; + PortIdSet::iterator portEntry = openPortIds_.find(port_id); + if (portEntry == openPortIds_.end()) { // unknown port ID. + VLOG(1) << "unknown port: " << port_id; + response->SetInteger(kResultKey, RESULT_UNKNOWN_PORT); + return; + } + DCHECK(service_); + service_->CloseChannel(port_id); + openPortIds_.erase(portEntry); + response->SetInteger(kResultKey, RESULT_OK); +} + +void ExtensionPortsRemoteService::PostMessageCommand( + int port_id, DictionaryValue* content, DictionaryValue* response) { + Value* data; + if (!content->Get(kDataKey, &data)) { + response->SetInteger(kResultKey, RESULT_PARAMETER_ERROR); + return; + } + std::string message; + // Stringified the JSON message body. + base::JSONWriter::Write(data, false, &message); + VLOG(1) << "postMessage: port " << port_id + << ", message: <" << message << ">"; + PortIdSet::iterator portEntry = openPortIds_.find(port_id); + if (portEntry == openPortIds_.end()) { // Unknown port ID. + VLOG(1) << "unknown port: " << port_id; + response->SetInteger(kResultKey, RESULT_UNKNOWN_PORT); + return; + } + // Post the message through the ExtensionMessageService. + DCHECK(service_); + service_->PostMessageFromRenderer(port_id, message); + // Confirm to the external client that we sent its message. + response->SetInteger(kResultKey, RESULT_OK); +} diff --git a/chrome/browser/debugger/extension_ports_remote_service.h b/chrome/browser/debugger/extension_ports_remote_service.h new file mode 100644 index 0000000..0a5eea6 --- /dev/null +++ b/chrome/browser/debugger/extension_ports_remote_service.h @@ -0,0 +1,115 @@ +// Copyright (c) 2011 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. + +// ExtensionsPorts service: wires extension message ports through the +// devtools remote protocol, allowing an external client program to +// exchange messages with Chrome extensions. + +#ifndef CHROME_BROWSER_DEBUGGER_EXTENSION_PORTS_REMOTE_SERVICE_H_ +#define CHROME_BROWSER_DEBUGGER_EXTENSION_PORTS_REMOTE_SERVICE_H_ +#pragma once + +#include <set> +#include <string> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "chrome/browser/debugger/devtools_remote.h" +#include "chrome/browser/extensions/extension_message_service.h" +#include "ipc/ipc_message.h" + +class DevToolsProtocolHandler; +class DevToolsRemoteMessage; +class GURL; + +namespace base { +class DictionaryValue; +class ListValue; +class Value; +} + +class ExtensionPortsRemoteService : public DevToolsRemoteListener, + public IPC::Message::Sender { + public: + // Specifies a tool name ("ExtensionPorts") handled by this class. + static const std::string kToolName; + + // |delegate| (never NULL) is the protocol handler instance which + // dispatches messages to this service. + // The ownership of |delegate| is NOT transferred to this class. + explicit ExtensionPortsRemoteService(DevToolsProtocolHandler* delegate); + + // DevToolsRemoteListener methods: + + // Processes |message| from the external client (where the tool is + // "ExtensionPorts"). + virtual void HandleMessage(const DevToolsRemoteMessage& message); + + // Gets invoked on the external client socket connection loss. + // Closes open message ports. + virtual void OnConnectionLost(); + + // IPC::Message::Sender methods: + + // This is the callback through which the ExtensionMessageService + // passes us messages from extensions as well as disconnect events. + virtual bool Send(IPC::Message* msg); + + private: + // Operation result returned in the "result" field in messages sent + // to the external client. + typedef enum { + RESULT_OK = 0, + RESULT_UNKNOWN_COMMAND, + RESULT_NO_SERVICE, + RESULT_PARAMETER_ERROR, + RESULT_UNKNOWN_PORT, + RESULT_TAB_NOT_FOUND, + RESULT_CONNECT_FAILED, // probably extension ID not found. + } Result; + + virtual ~ExtensionPortsRemoteService(); + + // Sends a JSON message with the |response| to the external client. + // |tool| and |destination| are used as the respective header values. + void SendResponse(const base::Value& response, + const std::string& tool, + const std::string& destination); + + // Handles a message from the ExtensionMessageService. + void OnExtensionMessageInvoke(const std::string& extension_id, + const std::string& function_name, + const base::ListValue& args, + const GURL& event_url); + // Handles a message sent from an extension through the + // ExtensionMessageService, to be passed to the external client. + void OnExtensionMessage(const std::string& message, int port_id); + // Handles a disconnect event sent from the ExtensionMessageService. + void OnExtensionPortDisconnected(int port_id); + + // Implementation for the commands we can receive from the external client. + // Opens a channel to an extension. + void ConnectCommand(base::DictionaryValue* content, + base::DictionaryValue* response); + // Disconnects a message port. + void DisconnectCommand(int port_id, base::DictionaryValue* response); + // Sends a message to an extension through an established message port. + void PostMessageCommand(int port_id, + base::DictionaryValue* content, + base::DictionaryValue* response); + + // The delegate is used to send responses and events back to the + // external client, and to resolve tab IDs. + DevToolsProtocolHandler* delegate_; + + // Set of message port IDs we successfully opened. + typedef std::set<int> PortIdSet; + PortIdSet openPortIds_; + + scoped_refptr<ExtensionMessageService> service_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionPortsRemoteService); +}; + +#endif // CHROME_BROWSER_DEBUGGER_EXTENSION_PORTS_REMOTE_SERVICE_H_ diff --git a/chrome/browser/debugger/inspectable_tab_proxy.cc b/chrome/browser/debugger/inspectable_tab_proxy.cc new file mode 100644 index 0000000..7008179 --- /dev/null +++ b/chrome/browser/debugger/inspectable_tab_proxy.cc @@ -0,0 +1,117 @@ +// Copyright (c) 2011 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/inspectable_tab_proxy.h" + +#include "base/string_number_conversions.h" +#include "base/string_util.h" +#include "chrome/browser/debugger/debugger_remote_service.h" +#include "chrome/browser/sessions/restore_tab_helper.h" +#include "chrome/browser/sessions/session_id.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "content/browser/debugger/devtools_client_host.h" +#include "content/browser/tab_contents/tab_contents.h" +#include "content/common/devtools_messages.h" + +DevToolsClientHostImpl::DevToolsClientHostImpl( + int32 id, + DebuggerRemoteService* service, + InspectableTabProxy::IdToClientHostMap* map) + : id_(id), + service_(service), + map_(map) {} + +DevToolsClientHostImpl::~DevToolsClientHostImpl() { + map_->erase(this->id_); +} + +// The debugged tab has closed. +void DevToolsClientHostImpl::InspectedTabClosing() { + TabClosed(); + delete this; +} + +// The remote debugger has detached. +void DevToolsClientHostImpl::CloseImpl() { + NotifyCloseListener(); + delete this; +} + +void DevToolsClientHostImpl::SendMessageToClient( + const IPC::Message& msg) { + // TODO(prybin): Restore FrameNavigate. + IPC_BEGIN_MESSAGE_MAP(DevToolsClientHostImpl, msg) + IPC_MESSAGE_HANDLER(DevToolsClientMsg_DebuggerOutput, OnDebuggerOutput); + IPC_MESSAGE_UNHANDLED_ERROR() + IPC_END_MESSAGE_MAP() +} + +void DevToolsClientHostImpl::TabReplaced(TabContents* new_tab) { + map_->erase(id_); + TabContentsWrapper* new_tab_wrapper = + TabContentsWrapper::GetCurrentWrapperForContents(new_tab); + DCHECK(new_tab_wrapper); + if (!new_tab_wrapper) + return; + id_ = new_tab_wrapper->restore_tab_helper()->session_id().id(); + (*map_)[id_] = this; +} + +void DevToolsClientHostImpl::OnDebuggerOutput(const std::string& data) { + service_->DebuggerOutput(id_, data); +} + +void DevToolsClientHostImpl::FrameNavigating(const std::string& url) { + service_->FrameNavigate(id_, url); +} + +void DevToolsClientHostImpl::TabClosed() { + service_->TabClosed(id_); +} + +InspectableTabProxy::InspectableTabProxy() {} + +InspectableTabProxy::~InspectableTabProxy() {} + +const InspectableTabProxy::TabMap& InspectableTabProxy::tab_map() { + tab_map_.clear(); + for (BrowserList::const_iterator it = BrowserList::begin(), + end = BrowserList::end(); it != end; ++it) { + TabStripModel* model = (*it)->tabstrip_model(); + for (int i = 0, size = model->count(); i < size; ++i) { + TabContentsWrapper* tab = model->GetTabContentsAt(i); + tab_map_[tab->restore_tab_helper()->session_id().id()] = tab; + } + } + return tab_map_; +} + +DevToolsClientHostImpl* InspectableTabProxy::ClientHostForTabId( + int32 id) { + InspectableTabProxy::IdToClientHostMap::const_iterator it = + id_to_client_host_map_.find(id); + if (it == id_to_client_host_map_.end()) { + return NULL; + } + return it->second; +} + +DevToolsClientHost* InspectableTabProxy::NewClientHost( + int32 id, + DebuggerRemoteService* service) { + DevToolsClientHostImpl* client_host = + new DevToolsClientHostImpl(id, service, &id_to_client_host_map_); + id_to_client_host_map_[id] = client_host; + return client_host; +} + +void InspectableTabProxy::OnRemoteDebuggerDetached() { + while (!id_to_client_host_map_.empty()) { + IdToClientHostMap::iterator it = id_to_client_host_map_.begin(); + it->second->debugger_remote_service()->DetachFromTab( + base::IntToString(it->first), NULL); + } +} diff --git a/chrome/browser/debugger/inspectable_tab_proxy.h b/chrome/browser/debugger/inspectable_tab_proxy.h new file mode 100644 index 0000000..0ccaf76 --- /dev/null +++ b/chrome/browser/debugger/inspectable_tab_proxy.h @@ -0,0 +1,91 @@ +// Copyright (c) 2011 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_INSPECTABLE_TAB_PROXY_H_ +#define CHROME_BROWSER_DEBUGGER_INSPECTABLE_TAB_PROXY_H_ +#pragma once + +#include <string> + +#include "base/basictypes.h" +#include "base/hash_tables.h" +#include "content/browser/debugger/devtools_client_host.h" + +class DebuggerRemoteService; +class DevToolsClientHost; +class DevToolsClientHostImpl; +struct DevToolsMessageData; +class TabContentsWrapper; + +// Proxies debugged tabs' TabContentsWrapper using their UIDs. +// Keeps track of tabs being debugged so that we can detach from +// them on remote debugger connection loss. +class InspectableTabProxy { + public: + typedef base::hash_map<int32, TabContentsWrapper*> TabMap; + typedef base::hash_map<int32, DevToolsClientHostImpl*> IdToClientHostMap; + + InspectableTabProxy(); + virtual ~InspectableTabProxy(); + + // Returns a map of SessionID to TabContentsWrapper for all Browser + // instances. Clients should not keep the result around for extended periods + // of time as tabs might get closed thus invalidating the map. + const TabMap& tab_map(); + + // Returns a DevToolsClientHostImpl for the given tab |id|. + DevToolsClientHostImpl* ClientHostForTabId(int32 id); + + // Creates a new DevToolsClientHost implementor instance. + // |id| is the UID of the tab to debug. + // |service| is the DebuggerRemoteService instance the DevToolsClient + // messages shall be dispatched to. + DevToolsClientHost* NewClientHost(int32 id, + DebuggerRemoteService* service); + + // Gets invoked when a remote debugger is detached. In this case we should + // send the corresponding message to the V8 debugger for each of the tabs + // the debugger is attached to, and invoke InspectedTabClosing(). + void OnRemoteDebuggerDetached(); + + private: + TabMap tab_map_; + IdToClientHostMap id_to_client_host_map_; + DISALLOW_COPY_AND_ASSIGN(InspectableTabProxy); +}; + + +// An internal implementation of DevToolsClientHost that delegates +// messages sent for DevToolsClient to a DebuggerShell instance. +class DevToolsClientHostImpl : public DevToolsClientHost { + public: + DevToolsClientHostImpl( + int32 id, + DebuggerRemoteService* service, + InspectableTabProxy::IdToClientHostMap* map); + virtual ~DevToolsClientHostImpl(); + + DebuggerRemoteService* debugger_remote_service() { + return service_; + } + + void CloseImpl(); + + // DevToolsClientHost interface + virtual void InspectedTabClosing(); + virtual void SendMessageToClient(const IPC::Message& msg); + virtual void TabReplaced(TabContents* new_tab); + + private: + // Message handling routines + void OnDebuggerOutput(const std::string& msg); + virtual void FrameNavigating(const std::string& url); + void TabClosed(); + + int32 id_; + DebuggerRemoteService* service_; + InspectableTabProxy::IdToClientHostMap* map_; +}; + +#endif // CHROME_BROWSER_DEBUGGER_INSPECTABLE_TAB_PROXY_H_ |