diff --git a/atom/browser/api/atom_api_debugger.cc b/atom/browser/api/atom_api_debugger.cc new file mode 100644 index 0000000000..eab60311f3 --- /dev/null +++ b/atom/browser/api/atom_api_debugger.cc @@ -0,0 +1,195 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_debugger.h" + +#include + +#include "atom/browser/atom_browser_main_parts.h" +#include "atom/common/native_mate_converters/callback.h" +#include "atom/common/native_mate_converters/value_converter.h" +#include "atom/common/node_includes.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "content/public/browser/devtools_agent_host.h" +#include "content/public/browser/web_contents.h" +#include "native_mate/dictionary.h" +#include "native_mate/object_template_builder.h" + +using content::DevToolsAgentHost; + +namespace atom { + +namespace api { + +namespace { + +// The wrapDebugger funtion which is implemented in JavaScript. +using WrapDebuggerCallback = base::Callback)>; +WrapDebuggerCallback g_wrap_debugger; + +} // namespace + +Debugger::Debugger(content::WebContents* web_contents) + : web_contents_(web_contents), + previous_request_id_(0) { +} + +Debugger::~Debugger() { +} + +void Debugger::AgentHostClosed(DevToolsAgentHost* agent_host, + bool replaced_with_another_client) { + std::string detach_reason = "target closed"; + if (replaced_with_another_client) + detach_reason = "replaced with devtools"; + Emit("detach", detach_reason); +} + +void Debugger::DispatchProtocolMessage(DevToolsAgentHost* agent_host, + const std::string& message) { + DCHECK(agent_host == agent_host_.get()); + + scoped_ptr parsed_message(base::JSONReader::Read(message)); + if (!parsed_message->IsType(base::Value::TYPE_DICTIONARY)) + return; + + base::DictionaryValue* dict = + static_cast(parsed_message.get()); + int id; + if (!dict->GetInteger("id", &id)) { + std::string method; + if (!dict->GetString("method", &method)) + return; + base::DictionaryValue* params_value = nullptr; + base::DictionaryValue params; + if (dict->GetDictionary("params", ¶ms_value)) + params.Swap(params_value); + Emit("message", method, params); + } else { + auto send_command_callback = pending_requests_[id]; + pending_requests_.erase(id); + if (send_command_callback.is_null()) + return; + base::DictionaryValue* error_body = nullptr; + base::DictionaryValue error; + if (dict->GetDictionary("error", &error_body)) + error.Swap(error_body); + + base::DictionaryValue* result_body = nullptr; + base::DictionaryValue result; + if (dict->GetDictionary("result", &result_body)) + result.Swap(result_body); + send_command_callback.Run(error, result); + } +} + +void Debugger::Attach(mate::Arguments* args) { + std::string protocol_version; + args->GetNext(&protocol_version); + + if (!protocol_version.empty() && + !DevToolsAgentHost::IsSupportedProtocolVersion(protocol_version)) { + args->ThrowError("Requested protocol version is not supported"); + return; + } + agent_host_ = DevToolsAgentHost::GetOrCreateFor(web_contents_); + if (!agent_host_.get()) { + args->ThrowError("No target available"); + return; + } + if (agent_host_->IsAttached()) { + args->ThrowError("Another debugger is already attached to this target"); + return; + } + + agent_host_->AttachClient(this); +} + +bool Debugger::IsAttached() { + return agent_host_.get() ? agent_host_->IsAttached() : false; +} + +void Debugger::Detach() { + if (!agent_host_.get()) + return; + agent_host_->DetachClient(); + AgentHostClosed(agent_host_.get(), false); + agent_host_ = nullptr; +} + +void Debugger::SendCommand(mate::Arguments* args) { + if (!agent_host_.get()) + return; + + std::string method; + if (!args->GetNext(&method)) { + args->ThrowError(); + return; + } + base::DictionaryValue command_params; + args->GetNext(&command_params); + SendCommandCallback callback; + args->GetNext(&callback); + + base::DictionaryValue request; + int request_id = ++previous_request_id_; + pending_requests_[request_id] = callback; + request.SetInteger("id", request_id); + request.SetString("method", method); + if (!command_params.empty()) + request.Set("params", command_params.DeepCopy()); + + std::string json_args; + base::JSONWriter::Write(request, &json_args); + agent_host_->DispatchProtocolMessage(json_args); +} + +// static +mate::Handle Debugger::Create( + v8::Isolate* isolate, + content::WebContents* web_contents) { + auto handle = mate::CreateHandle(isolate, new Debugger(web_contents)); + g_wrap_debugger.Run(handle.ToV8()); + return handle; +} + +// static +void Debugger::BuildPrototype(v8::Isolate* isolate, + v8::Local prototype) { + mate::ObjectTemplateBuilder(isolate, prototype) + .SetMethod("attach", &Debugger::Attach) + .SetMethod("isAttached", &Debugger::IsAttached) + .SetMethod("detach", &Debugger::Detach) + .SetMethod("sendCommand", &Debugger::SendCommand); +} + +void ClearWrapDebugger() { + g_wrap_debugger.Reset(); +} + +void SetWrapDebugger(const WrapDebuggerCallback& callback) { + g_wrap_debugger = callback; + + // Cleanup the wrapper on exit. + atom::AtomBrowserMainParts::Get()->RegisterDestructionCallback( + base::Bind(ClearWrapDebugger)); +} + +} // namespace api + +} // namespace atom + +namespace { + +void Initialize(v8::Local exports, v8::Local unused, + v8::Local context, void* priv) { + v8::Isolate* isolate = context->GetIsolate(); + mate::Dictionary dict(isolate, exports); + dict.SetMethod("_setWrapDebugger", &atom::api::SetWrapDebugger); +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_debugger, Initialize); diff --git a/atom/browser/api/atom_api_debugger.h b/atom/browser/api/atom_api_debugger.h new file mode 100644 index 0000000000..5454108e8b --- /dev/null +++ b/atom/browser/api/atom_api_debugger.h @@ -0,0 +1,75 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_DEBUGGER_H_ +#define ATOM_BROWSER_API_ATOM_API_DEBUGGER_H_ + +#include +#include + +#include "atom/browser/api/trackable_object.h" +#include "base/callback.h" +#include "base/values.h" +#include "content/public/browser/devtools_agent_host_client.h" +#include "native_mate/handle.h" + +namespace content { +class DevToolsAgentHost; +class WebContents; +} + +namespace mate { +class Arguments; +} + +namespace atom { + +namespace api { + +class Debugger: public mate::TrackableObject, + public content::DevToolsAgentHostClient { + public: + using SendCommandCallback = + base::Callback; + + static mate::Handle Create( + v8::Isolate* isolate, content::WebContents* web_contents); + + // mate::TrackableObject: + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + protected: + explicit Debugger(content::WebContents* web_contents); + ~Debugger(); + + // content::DevToolsAgentHostClient: + void AgentHostClosed(content::DevToolsAgentHost* agent_host, + bool replaced_with_another_client) override; + void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host, + const std::string& message) override; + + private: + using PendingRequestMap = std::map; + + void Attach(mate::Arguments* args); + bool IsAttached(); + void Detach(); + void SendCommand(mate::Arguments* args); + + content::WebContents* web_contents_; // Weak Reference. + scoped_refptr agent_host_; + + PendingRequestMap pending_requests_; + int previous_request_id_; + + DISALLOW_COPY_AND_ASSIGN(Debugger); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_DEBUGGER_H_ diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 0173abf4ee..2b14bdc60d 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -7,6 +7,7 @@ #include #include +#include "atom/browser/api/atom_api_debugger.h" #include "atom/browser/api/atom_api_session.h" #include "atom/browser/api/atom_api_window.h" #include "atom/browser/atom_browser_client.h" @@ -1076,6 +1077,14 @@ v8::Local WebContents::DevToolsWebContents(v8::Isolate* isolate) { return v8::Local::New(isolate, devtools_web_contents_); } +v8::Local WebContents::Debugger(v8::Isolate* isolate) { + if (debugger_.IsEmpty()) { + auto handle = atom::api::Debugger::Create(isolate, web_contents()); + debugger_.Reset(isolate, handle.ToV8()); + } + return v8::Local::New(isolate, debugger_); +} + // static void WebContents::BuildPrototype(v8::Isolate* isolate, v8::Local prototype) { @@ -1144,7 +1153,8 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("addWorkSpace", &WebContents::AddWorkSpace) .SetMethod("removeWorkSpace", &WebContents::RemoveWorkSpace) .SetProperty("session", &WebContents::Session) - .SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents); + .SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents) + .SetProperty("debugger", &WebContents::Debugger); } AtomBrowserContext* WebContents::GetBrowserContext() const { diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index bcef57b9a4..10ac7a4f76 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -142,6 +142,7 @@ class WebContents : public mate::TrackableObject, // Properties. v8::Local Session(v8::Isolate* isolate); v8::Local DevToolsWebContents(v8::Isolate* isolate); + v8::Local Debugger(v8::Isolate* isolate); // mate::TrackableObject: static void BuildPrototype(v8::Isolate* isolate, @@ -265,6 +266,7 @@ class WebContents : public mate::TrackableObject, v8::Global session_; v8::Global devtools_web_contents_; + v8::Global debugger_; scoped_ptr guest_delegate_; diff --git a/atom/browser/api/lib/web-contents.js b/atom/browser/api/lib/web-contents.js index 13e6e97900..709694bcf7 100644 --- a/atom/browser/api/lib/web-contents.js +++ b/atom/browser/api/lib/web-contents.js @@ -7,6 +7,7 @@ const NavigationController = require('electron').NavigationController; const Menu = require('electron').Menu; const binding = process.atomBinding('web_contents'); +const debuggerBinding = process.atomBinding('debugger'); let slice = [].slice; let nextId = 0; @@ -215,7 +216,14 @@ let wrapWebContents = function(webContents) { }; }; +// Wrapper for native class. +let wrapDebugger = function(webContentsDebugger) { + // debugger is an EventEmitter. + webContentsDebugger.__proto__ = EventEmitter.prototype; +}; + binding._setWrapWebContents(wrapWebContents); +debuggerBinding._setWrapDebugger(wrapDebugger); module.exports.create = function(options) { if (options == null) { diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index b1cb84eead..69e7906ffb 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -35,6 +35,7 @@ REFERENCE_MODULE(atom_browser_app); REFERENCE_MODULE(atom_browser_auto_updater); REFERENCE_MODULE(atom_browser_content_tracing); REFERENCE_MODULE(atom_browser_dialog); +REFERENCE_MODULE(atom_browser_debugger); REFERENCE_MODULE(atom_browser_desktop_capturer); REFERENCE_MODULE(atom_browser_download_item); REFERENCE_MODULE(atom_browser_menu); diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 48658a2945..939a096299 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -833,3 +833,73 @@ Get the `WebContents` of DevTools for this `WebContents`. **Note:** Users should never store this object because it may become `null` when the DevTools has been closed. + +### `webContents.debugger` + +Debugger API serves as an alternate transport for [remote debugging protocol][rdp]. + +```javascript +try { + win.webContents.debugger.attach("1.1"); +} catch(err) { + console.log("Debugger attach failed : ", err); +}; + +win.webContents.debugger.on('detach', function(event, reason) { + console.log("Debugger detached due to : ", reason); +}); + +win.webContents.debugger.on('message', function(event, method, params) { + if (method == "Network.requestWillBeSent") { + if (params.request.url == "https://www.github.com") + win.webContents.debugger.detach(); + } +}) + +win.webContents.debugger.sendCommand("Network.enable"); +``` + +#### `webContents.debugger.attach([protocolVersion])` + +* `protocolVersion` String (optional) - Requested debugging protocol version. + +Attaches the debugger to the `webContents`. + +#### `webContents.debugger.isAttached()` + +Returns a boolean indicating whether a debugger is attached to the `webContents`. + +#### `webContents.debugger.detach()` + +Detaches the debugger from the `webContents`. + +#### `webContents.debugger.sendCommand(method[, commandParams, callback])` + +* `method` String - Method name, should be one of the methods defined by the + remote debugging protocol. +* `commandParams` Object (optional) - JSON object with request parameters. +* `callback` Function (optional) - Response + * `error` Object - Error message indicating the failure of the command. + * `result` Object - Response defined by the 'returns' attribute of + the command description in the remote debugging protocol. + +Send given command to the debugging target. + +#### Event: 'detach' + +* `event` Event +* `reason` String - Reason for detaching debugger. + +Emitted when debugging session is terminated. This happens either when +`webContents` is closed or devtools is invoked for the attached `webContents`. + +#### Event: 'message' + +* `event` Event +* `method` String - Method name. +* `params` Object - Event parameters defined by the 'parameters' + attribute in the remote debugging protocol. + +Emitted whenever debugging target issues instrumentation event. + +[rdp]: https://developer.chrome.com/devtools/docs/debugger-protocol diff --git a/filenames.gypi b/filenames.gypi index 61aa3d43d4..4aa47fc358 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -83,6 +83,8 @@ 'atom/browser/api/atom_api_content_tracing.cc', 'atom/browser/api/atom_api_cookies.cc', 'atom/browser/api/atom_api_cookies.h', + 'atom/browser/api/atom_api_debugger.cc', + 'atom/browser/api/atom_api_debugger.h', 'atom/browser/api/atom_api_desktop_capturer.cc', 'atom/browser/api/atom_api_desktop_capturer.h', 'atom/browser/api/atom_api_download_item.cc', diff --git a/spec/api-debugger-spec.js b/spec/api-debugger-spec.js new file mode 100644 index 0000000000..7a16ca7263 --- /dev/null +++ b/spec/api-debugger-spec.js @@ -0,0 +1,136 @@ +var assert, path, remote, BrowserWindow; + +assert = require('assert'); + +path = require('path'); + +remote = require('electron').remote; + +BrowserWindow = remote.BrowserWindow; + +describe('debugger module', function() { + var fixtures, w; + fixtures = path.resolve(__dirname, 'fixtures'); + w = null; + beforeEach(function() { + if (w != null) { + w.destroy(); + } + w = new BrowserWindow({ + show: false, + width: 400, + height: 400 + }); + }); + afterEach(function() { + if (w != null) { + w.destroy(); + } + w = null; + }); + + describe('debugger.attach', function() { + it('fails when devtools is already open', function(done) { + w.webContents.on('did-finish-load', function() { + w.webContents.openDevTools(); + try { + w.webContents.debugger.attach(); + } catch(err) { + assert(w.webContents.debugger.isAttached()); + done(); + } + }); + w.webContents.loadURL('file://' + path.join(fixtures, 'pages', 'a.html')); + }); + + it('fails when protocol version is not supported', function(done) { + try { + w.webContents.debugger.attach("2.0"); + } catch(err) { + assert(!w.webContents.debugger.isAttached()); + done(); + } + }); + + it('attaches when no protocol version is specified', function(done) { + try { + w.webContents.debugger.attach(); + } catch(err) { + done('unexpected error : ' + err); + } + assert(w.webContents.debugger.isAttached()); + done(); + }); + }); + + describe('debugger.detach', function() { + it('fires detach event', function(done) { + w.webContents.debugger.on('detach', function(e, reason) { + assert.equal(reason, 'target closed'); + assert(!w.webContents.debugger.isAttached()); + done(); + }); + try { + w.webContents.debugger.attach(); + } catch(err) { + done('unexpected error : ' + err); + } + w.webContents.debugger.detach(); + }); + }); + + describe('debugger.sendCommand', function() { + it('retuns response', function(done) { + w.webContents.loadURL('about:blank'); + try { + w.webContents.debugger.attach(); + } catch(err) { + done('unexpected error : ' + err); + } + var callback = function(err, res) { + assert(!res.wasThrown); + assert.equal(res.result.value, 6); + w.webContents.debugger.detach(); + done(); + }; + const params = { + "expression": "4+2", + }; + w.webContents.debugger.sendCommand("Runtime.evaluate", params, callback); + }); + + it('fires message event', function(done) { + var url = 'file://' + path.join(fixtures, 'pages', 'a.html'); + w.webContents.loadURL(url); + try { + w.webContents.debugger.attach(); + } catch(err) { + done('unexpected error : ' + err); + } + w.webContents.debugger.on('message', function(e, method, params) { + if(method == "Console.messageAdded") { + assert.equal(params.message.type, 'log'); + assert.equal(params.message.url, url); + assert.equal(params.message.text, 'a'); + w.webContents.debugger.detach(); + done(); + } + }); + w.webContents.debugger.sendCommand("Console.enable"); + }); + + it('returns error message when command fails', function(done) { + w.webContents.loadURL('about:blank'); + try { + w.webContents.debugger.attach(); + } catch(err) { + done('unexpected error : ' + err); + } + w.webContents.debugger.sendCommand("Test", function(err) { + assert.equal(err.message, '\'Test\' wasn\'t found'); + w.webContents.debugger.detach(); + done(); + }); + }); + }); +});