Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: promisify debugger.sendCommand() (backport: 5-0-x) #16931

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 32 additions & 29 deletions atom/browser/api/atom_api_debugger.cc
Expand Up @@ -61,23 +61,26 @@ void Debugger::DispatchProtocolMessage(DevToolsAgentHost* agent_host,
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())
auto it = pending_requests_.find(id);
if (it == pending_requests_.end())
return;
base::DictionaryValue* error_body = nullptr;
base::DictionaryValue error;
bool has_error;
if ((has_error = 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(has_error ? error.Clone() : base::Value(),
result);
scoped_refptr<atom::util::Promise> promise = it->second;
pending_requests_.erase(it);

base::DictionaryValue* error = nullptr;
if (dict->GetDictionary("error", &error)) {
std::string message;
error->GetString("message", &message);
promise->RejectWithErrorMessage(message);
} else {
base::DictionaryValue* result_body = nullptr;
base::DictionaryValue result;
if (dict->GetDictionary("result", &result_body)) {
result.Swap(result_body);
}
promise->Resolve(result);
}
}
}

Expand Down Expand Up @@ -125,23 +128,26 @@ void Debugger::Detach() {
AgentHostClosed(agent_host_.get());
}

void Debugger::SendCommand(mate::Arguments* args) {
if (!agent_host_)
return;
v8::Local<v8::Promise> Debugger::SendCommand(mate::Arguments* args) {
scoped_refptr<util::Promise> promise = new util::Promise(isolate());

if (!agent_host_) {
promise->RejectWithErrorMessage("No target available");
return promise->GetHandle();
}

std::string method;
if (!args->GetNext(&method)) {
args->ThrowError();
return;
promise->RejectWithErrorMessage("Invalid method");
return promise->GetHandle();
}

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;
pending_requests_[request_id] = promise;
request.SetInteger("id", request_id);
request.SetString("method", method);
if (!command_params.empty())
Expand All @@ -151,16 +157,13 @@ void Debugger::SendCommand(mate::Arguments* args) {
std::string json_args;
base::JSONWriter::Write(request, &json_args);
agent_host_->DispatchProtocolMessage(this, json_args);

return promise->GetHandle();
}

void Debugger::ClearPendingRequests() {
if (pending_requests_.empty())
return;
base::Value error(base::Value::Type::DICTIONARY);
base::Value error_msg("target closed while handling command");
error.SetKey("message", std::move(error_msg));
for (const auto& it : pending_requests_)
it.second.Run(error, base::Value());
it.second->RejectWithErrorMessage("target closed while handling command");
}

// static
Expand Down
8 changes: 3 additions & 5 deletions atom/browser/api/atom_api_debugger.h
Expand Up @@ -9,6 +9,7 @@
#include <string>

#include "atom/browser/api/trackable_object.h"
#include "atom/common/promise_util.h"
#include "base/callback.h"
#include "base/values.h"
#include "content/public/browser/devtools_agent_host_client.h"
Expand All @@ -32,9 +33,6 @@ class Debugger : public mate::TrackableObject<Debugger>,
public content::DevToolsAgentHostClient,
public content::WebContentsObserver {
public:
using SendCommandCallback =
base::Callback<void(const base::Value&, const base::Value&)>;

static mate::Handle<Debugger> Create(v8::Isolate* isolate,
content::WebContents* web_contents);

Expand All @@ -56,12 +54,12 @@ class Debugger : public mate::TrackableObject<Debugger>,
content::RenderFrameHost* new_rfh) override;

private:
using PendingRequestMap = std::map<int, SendCommandCallback>;
using PendingRequestMap = std::map<int, scoped_refptr<atom::util::Promise>>;

void Attach(mate::Arguments* args);
bool IsAttached();
void Detach();
void SendCommand(mate::Arguments* args);
v8::Local<v8::Promise> SendCommand(mate::Arguments* args);
void ClearPendingRequests();

content::WebContents* web_contents_; // Weak Reference.
Expand Down
14 changes: 14 additions & 0 deletions docs/api/debugger.md
Expand Up @@ -60,6 +60,20 @@ Detaches the debugger from the `webContents`.

Send given command to the debugging target.

**[Deprecated Soon](promisification.md)**

#### `debugger.sendCommand(method[, commandParams])`

* `method` String - Method name, should be one of the methods defined by the
[remote debugging protocol][rdp].
* `commandParams` Object (optional) - JSON object with request parameters.

Returns `Promise<any>` - A promise that resolves with the response defined by
the 'returns' attribute of the command description in the remote debugging protocol
or is rejected indicating the failure of the command.

Send given command to the debugging target.

### Instance Events

#### Event: 'detach'
Expand Down
2 changes: 1 addition & 1 deletion docs/api/promisification.md
Expand Up @@ -10,7 +10,6 @@ When a majority of affected functions are migrated, this flag will be enabled by

- [app.importCertificate(options, callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#importCertificate)
- [contentTracing.getTraceBufferUsage(callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#getTraceBufferUsage)
- [debugger.sendCommand(method[, commandParams, callback])](https://github.com/electron/electron/blob/master/docs/api/debugger.md#sendCommand)
- [dialog.showOpenDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showOpenDialog)
- [dialog.showSaveDialog([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showSaveDialog)
- [dialog.showMessageBox([browserWindow, ]options[, callback])](https://github.com/electron/electron/blob/master/docs/api/dialog.md#showMessageBox)
Expand Down Expand Up @@ -49,6 +48,7 @@ When a majority of affected functions are migrated, this flag will be enabled by
- [cookies.get(filter, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#get)
- [cookies.remove(url, name, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#remove)
- [cookies.set(details, callback)](https://github.com/electron/electron/blob/master/docs/api/cookies.md#set)
- [debugger.sendCommand(method[, commandParams, callback])](https://github.com/electron/electron/blob/master/docs/api/debugger.md#sendCommand)
- [desktopCapturer.getSources(options, callback)](https://github.com/electron/electron/blob/master/docs/api/desktop-capturer.md#getSources)
- [protocol.isProtocolHandled(scheme, callback)](https://github.com/electron/electron/blob/master/docs/api/protocol.md#isProtocolHandled)
- [shell.openExternal(url[, options, callback])](https://github.com/electron/electron/blob/master/docs/api/shell.md#openExternal)
Expand Down
2 changes: 2 additions & 0 deletions lib/browser/api/web-contents.js
Expand Up @@ -501,6 +501,8 @@ WebContents.prototype._init = function () {
// JavaScript wrapper of Debugger.
const { Debugger } = process.atomBinding('debugger')

Debugger.prototype.sendCommand = deprecate.promisify(Debugger.prototype.sendCommand)

Object.setPrototypeOf(Debugger.prototype, EventEmitter.prototype)

// Public APIs.
Expand Down
54 changes: 48 additions & 6 deletions spec/api-debugger-spec.js
Expand Up @@ -2,6 +2,7 @@ const chai = require('chai')
const dirtyChai = require('dirty-chai')
const http = require('http')
const path = require('path')
const { emittedOnce } = require('./events-helpers')
const { closeWindow } = require('./window-helpers')
const { BrowserWindow } = require('electron').remote

Expand Down Expand Up @@ -102,7 +103,21 @@ describe('debugger module', () => {
}
})

it('returns response', done => {
it('returns response', async () => {
w.webContents.loadURL('about:blank')
w.webContents.debugger.attach()

const params = { 'expression': '4+2' }
const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params)

expect(res.wasThrown).to.be.undefined()
expect(res.result.value).to.equal(6)

w.webContents.debugger.detach()
})

// TODO(miniak): remove when promisification is complete
it('returns response (callback)', done => {
w.webContents.loadURL('about:blank')
try {
w.webContents.debugger.attach()
Expand All @@ -123,7 +138,24 @@ describe('debugger module', () => {
w.webContents.debugger.sendCommand('Runtime.evaluate', params, callback)
})

it('returns response when devtools is opened', done => {
it('returns response when devtools is opened', async () => {
w.webContents.loadURL('about:blank')
w.webContents.debugger.attach()

w.webContents.openDevTools()
await emittedOnce(w.webContents, 'devtools-opened')

const params = { 'expression': '4+2' }
const res = await w.webContents.debugger.sendCommand('Runtime.evaluate', params)

expect(res.wasThrown).to.be.undefined()
expect(res.result.value).to.equal(6)

w.webContents.debugger.detach()
})

// TODO(miniak): remove when promisification is complete
it('returns response when devtools is opened (callback)', done => {
w.webContents.loadURL('about:blank')
try {
w.webContents.debugger.attach()
Expand Down Expand Up @@ -169,17 +201,27 @@ describe('debugger module', () => {
w.webContents.debugger.sendCommand('Console.enable')
})

it('returns error message when command fails', done => {
it('returns error message when command fails', async () => {
w.webContents.loadURL('about:blank')
w.webContents.debugger.attach()

const promise = w.webContents.debugger.sendCommand('Test')
await expect(promise).to.be.eventually.rejectedWith(Error, "'Test' wasn't found")

w.webContents.debugger.detach()
})

// TODO(miniak): remove when promisification is complete
it('returns error message when command fails (callback)', done => {
w.webContents.loadURL('about:blank')
try {
w.webContents.debugger.attach()
} catch (err) {
done(`unexpected error : ${err}`)
}

w.webContents.debugger.sendCommand('Test', err => {
expect(err).to.not.be.null()
expect(err.message).to.equal("'Test' wasn't found")
w.webContents.debugger.sendCommand('Test', (err, res) => {
expect(err).to.be.an.instanceOf(Error).with.property('message', "'Test' wasn't found")
w.webContents.debugger.detach()
done()
})
Expand Down
6 changes: 1 addition & 5 deletions spec/chromium-spec.js
Expand Up @@ -1515,11 +1515,7 @@ describe('font fallback', () => {
try {
await w.loadURL(`data:text/html,${html}`)
w.webContents.debugger.attach()
const sendCommand = (...args) => new Promise((resolve, reject) => {
w.webContents.debugger.sendCommand(...args, (e, r) => {
if (e) { reject(e) } else { resolve(r) }
})
})
const sendCommand = (...args) => w.webContents.debugger.sendCommand(...args)
const { nodeId } = (await sendCommand('DOM.getDocument')).root.children[0]
await sendCommand('CSS.enable')
const { fonts } = await sendCommand('CSS.getPlatformFontsForNode', { nodeId })
Expand Down