Add support for direct DevTools protocol messaging (fixes issue #2961).

This change allows the client to directly send and receive DevTools
protocol messages (send method calls, and receive method results and
events) without requiring a DevTools front-end or remote-debugging
session.

This change includes additional supporting changes:
- Add a new CefRequestHandler::OnDocumentAvailableInMainFrame
  callback (see issue #1454).
- Add a CefParseJSON variant that accepts a UTF8-encoded buffer.
- Add a `--devtools-protocol-log-file=<path>` command-line flag for
  logging protocol messages sent to/from the DevTools front-end
  while it is displayed. This is useful for understanding existing
  DevTools protocol usage.
- Add a new "libcef_static_unittests" executable target to support
  light-weight unit tests of libcef_static internals (e.g. without
  requiring exposure via the CEF API). Files to be unittested are
  placed in the new "libcef_static_unittested" source_set which is
  then included by both the existing libcef_static library and the
  new unittests executable target.
- Linux: Remove use_bundled_fontconfig=false, which is no longer
  required and causes unittest build errors (see issue #2424).

This change also adds a cefclient demo for configuring offline mode
using the DevTools protocol (fixes issue #245). This is controlled
by the "Offline mode" context menu option and the `--offline`
command-line switch which will launch cefclient in offline mode. When
cefclient is offline all network requests will fail with
ERR_INTERNET_DISCONNECTED and navigator.onLine will return false when
called from JavaScript in any frame. This mode is per-browser so
newly created browser windows will have the default mode. Note that
configuring offline mode in this way will not update the Network tab
UI ("Throtting" option) in a displayed DevTools front-end instance.
This commit is contained in:
Marshall Greenblatt
2020-06-12 20:54:08 -04:00
parent a9aef28966
commit 39aed35644
46 changed files with 2824 additions and 146 deletions

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 "libcef/browser/devtools/devtools_controller.h"
#include "libcef/browser/devtools/devtools_util.h"
#include "libcef/browser/thread_util.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "content/public/browser/devtools_agent_host.h"
CefDevToolsController::CefDevToolsController(
content::WebContents* inspected_contents)
: inspected_contents_(inspected_contents), weak_ptr_factory_(this) {
DCHECK(inspected_contents_);
}
CefDevToolsController::~CefDevToolsController() {
if (agent_host_) {
agent_host_->DetachClient(this);
AgentHostClosed(agent_host_.get());
}
for (auto& observer : observers_) {
observer.OnDevToolsControllerDestroyed();
}
}
bool CefDevToolsController::SendDevToolsMessage(
const base::StringPiece& message) {
CEF_REQUIRE_UIT();
if (!EnsureAgentHost())
return false;
agent_host_->DispatchProtocolMessage(
this, base::as_bytes(base::make_span(message)));
return true;
}
int CefDevToolsController::ExecuteDevToolsMethod(
int suggested_message_id,
const std::string& method,
const base::DictionaryValue* params) {
CEF_REQUIRE_UIT();
if (!EnsureAgentHost())
return 0;
// Message IDs must always be increasing and unique.
int message_id = suggested_message_id;
if (message_id < next_message_id_)
message_id = next_message_id_++;
else
next_message_id_ = message_id + 1;
base::DictionaryValue message;
message.SetIntKey("id", message_id);
message.SetStringKey("method", method);
if (params)
message.SetKey("params", params->Clone());
std::string protocol_message;
if (!base::JSONWriter::Write(message, &protocol_message))
return 0;
agent_host_->DispatchProtocolMessage(
this, base::as_bytes(base::make_span(protocol_message)));
return message_id;
}
void CefDevToolsController::AgentHostClosed(
content::DevToolsAgentHost* agent_host) {
DCHECK(agent_host == agent_host_.get());
agent_host_ = nullptr;
for (auto& observer : observers_) {
observer.OnDevToolsAgentDetached();
}
}
void CefDevToolsController::AddObserver(Observer* observer) {
CEF_REQUIRE_UIT();
observers_.AddObserver(observer);
}
void CefDevToolsController::RemoveObserver(Observer* observer) {
CEF_REQUIRE_UIT();
observers_.RemoveObserver(observer);
}
void CefDevToolsController::DispatchProtocolMessage(
content::DevToolsAgentHost* agent_host,
base::span<const uint8_t> message) {
if (!observers_.might_have_observers())
return;
base::StringPiece str_message(reinterpret_cast<const char*>(message.data()),
message.size());
if (!devtools_util::ProtocolParser::IsValidMessage(str_message)) {
LOG(WARNING) << "Invalid message: " << str_message.substr(0, 100);
return;
}
devtools_util::ProtocolParser parser;
for (auto& observer : observers_) {
if (observer.OnDevToolsMessage(str_message)) {
continue;
}
// Only perform parsing a single time.
if (parser.Initialize(str_message) && parser.IsFailure()) {
LOG(WARNING) << "Failed to parse message: " << str_message.substr(0, 100);
}
if (parser.IsEvent()) {
observer.OnDevToolsEvent(parser.method_, parser.params_);
} else if (parser.IsResult()) {
observer.OnDevToolsMethodResult(parser.message_id_, parser.success_,
parser.params_);
}
}
}
bool CefDevToolsController::EnsureAgentHost() {
if (!agent_host_) {
agent_host_ =
content::DevToolsAgentHost::GetOrCreateFor(inspected_contents_);
if (agent_host_) {
agent_host_->AttachClient(this);
for (auto& observer : observers_) {
observer.OnDevToolsAgentAttached();
}
}
}
return !!agent_host_;
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_CONTROLLER_H_
#define CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_CONTROLLER_H_
#include <memory>
#include "content/public/browser/devtools_agent_host_client.h"
#include "base/containers/span.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/values.h"
namespace content {
class WebContents;
}
class CefDevToolsController : public content::DevToolsAgentHostClient {
public:
class Observer : public base::CheckedObserver {
public:
// See CefDevToolsMessageObserver documentation.
virtual bool OnDevToolsMessage(const base::StringPiece& message) = 0;
virtual void OnDevToolsMethodResult(int message_id,
bool success,
const base::StringPiece& result) = 0;
virtual void OnDevToolsEvent(const base::StringPiece& method,
const base::StringPiece& params) = 0;
virtual void OnDevToolsAgentAttached() = 0;
virtual void OnDevToolsAgentDetached() = 0;
virtual void OnDevToolsControllerDestroyed() = 0;
protected:
~Observer() override {}
};
// |inspected_contents| will outlive this object.
explicit CefDevToolsController(content::WebContents* inspected_contents);
~CefDevToolsController() override;
// See CefBrowserHost methods of the same name for documentation.
bool SendDevToolsMessage(const base::StringPiece& message);
int ExecuteDevToolsMethod(int message_id,
const std::string& method,
const base::DictionaryValue* params);
// |observer| must outlive this object or be removed.
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
base::WeakPtr<CefDevToolsController> GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
private:
// content::DevToolsAgentHostClient implementation:
void AgentHostClosed(content::DevToolsAgentHost* agent_host) override;
void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host,
base::span<const uint8_t> message) override;
bool EnsureAgentHost();
content::WebContents* const inspected_contents_;
scoped_refptr<content::DevToolsAgentHost> agent_host_;
int next_message_id_ = 1;
base::ObserverList<Observer> observers_;
base::WeakPtrFactory<CefDevToolsController> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(CefDevToolsController);
};
#endif // CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_CONTROLLER_H_

View File

@@ -6,13 +6,18 @@
#include <stddef.h>
#include <iomanip>
#include <utility>
#include "libcef/browser/browser_context.h"
#include "libcef/browser/content_browser_client.h"
#include "libcef/browser/devtools/devtools_manager_delegate.h"
#include "libcef/browser/net/devtools_scheme_handler.h"
#include "libcef/common/cef_switches.h"
#include "base/base64.h"
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/guid.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
@@ -48,6 +53,13 @@
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "storage/browser/file_system/native_file_util.h"
#if defined(OS_WIN)
#include <windows.h>
#elif defined(OS_POSIX)
#include <time.h>
#endif
namespace {
@@ -86,6 +98,71 @@ std::unique_ptr<base::DictionaryValue> BuildObjectForResponse(
return response;
}
const int kMaxLogLineLength = 1024;
void WriteTimestamp(std::stringstream& stream) {
#if defined(OS_WIN)
SYSTEMTIME local_time;
GetLocalTime(&local_time);
stream << std::setfill('0') << std::setw(2) << local_time.wMonth
<< std::setw(2) << local_time.wDay << '/' << std::setw(2)
<< local_time.wHour << std::setw(2) << local_time.wMinute
<< std::setw(2) << local_time.wSecond << '.' << std::setw(3)
<< local_time.wMilliseconds;
#elif defined(OS_POSIX)
timeval tv;
gettimeofday(&tv, nullptr);
time_t t = tv.tv_sec;
struct tm local_time;
localtime_r(&t, &local_time);
struct tm* tm_time = &local_time;
stream << std::setfill('0') << std::setw(2) << 1 + tm_time->tm_mon
<< std::setw(2) << tm_time->tm_mday << '/' << std::setw(2)
<< tm_time->tm_hour << std::setw(2) << tm_time->tm_min << std::setw(2)
<< tm_time->tm_sec << '.' << std::setw(6) << tv.tv_usec;
#else
#error Unsupported platform
#endif
}
void LogProtocolMessage(const base::FilePath& log_file,
ProtocolMessageType type,
std::string to_log) {
// Track if logging has failed, in which case we don't keep trying.
static bool log_error = false;
if (log_error)
return;
if (storage::NativeFileUtil::EnsureFileExists(log_file, nullptr) !=
base::File::FILE_OK) {
LOG(ERROR) << "Failed to create file " << log_file.value();
log_error = true;
return;
}
std::string type_label;
switch (type) {
case ProtocolMessageType::METHOD:
type_label = "METHOD";
break;
case ProtocolMessageType::RESULT:
type_label = "RESULT";
break;
case ProtocolMessageType::EVENT:
type_label = "EVENT";
break;
}
std::stringstream stream;
WriteTimestamp(stream);
stream << ": " << type_label << ": " << to_log << "\n";
const std::string& str = stream.str();
if (!base::AppendToFile(log_file, str.c_str(), str.size())) {
LOG(ERROR) << "Failed to write file " << log_file.value();
log_error = true;
}
}
} // namespace
class CefDevToolsFrontend::NetworkResourceLoader
@@ -156,11 +233,12 @@ const size_t kMaxMessageChunkSize = IPC::Channel::kMaximumMessageSize / 4;
// static
CefDevToolsFrontend* CefDevToolsFrontend::Show(
CefRefPtr<CefBrowserHostImpl> inspected_browser,
CefBrowserHostImpl* inspected_browser,
const CefWindowInfo& windowInfo,
CefRefPtr<CefClient> client,
const CefBrowserSettings& settings,
const CefPoint& inspect_element_at) {
const CefPoint& inspect_element_at,
base::OnceClosure frontend_destroyed_callback) {
CefBrowserSettings new_settings = settings;
if (!windowInfo.windowless_rendering_enabled &&
CefColorGetA(new_settings.background_color) != SK_AlphaOPAQUE) {
@@ -187,7 +265,8 @@ CefDevToolsFrontend* CefDevToolsFrontend::Show(
// destroyed.
CefDevToolsFrontend* devtools_frontend = new CefDevToolsFrontend(
static_cast<CefBrowserHostImpl*>(frontend_browser.get()),
inspected_contents, inspect_element_at);
inspected_contents, inspect_element_at,
std::move(frontend_destroyed_callback));
// Need to load the URL after creating the DevTools objects.
frontend_browser->GetMainFrame()->LoadURL(GetFrontendURL());
@@ -216,23 +295,23 @@ void CefDevToolsFrontend::Close() {
frontend_browser_.get(), true));
}
void CefDevToolsFrontend::DisconnectFromTarget() {
if (!agent_host_)
return;
agent_host_->DetachClient(this);
agent_host_ = nullptr;
}
CefDevToolsFrontend::CefDevToolsFrontend(
CefRefPtr<CefBrowserHostImpl> frontend_browser,
CefBrowserHostImpl* frontend_browser,
content::WebContents* inspected_contents,
const CefPoint& inspect_element_at)
const CefPoint& inspect_element_at,
base::OnceClosure frontend_destroyed_callback)
: content::WebContentsObserver(frontend_browser->web_contents()),
frontend_browser_(frontend_browser),
inspected_contents_(inspected_contents),
inspect_element_at_(inspect_element_at),
file_manager_(frontend_browser.get(), GetPrefs()),
weak_factory_(this) {}
frontend_destroyed_callback_(std::move(frontend_destroyed_callback)),
file_manager_(frontend_browser, GetPrefs()),
protocol_log_file_(
base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
switches::kDevToolsProtocolLogFile)),
weak_factory_(this) {
DCHECK(!frontend_destroyed_callback_.is_null());
}
CefDevToolsFrontend::~CefDevToolsFrontend() {}
@@ -279,6 +358,7 @@ void CefDevToolsFrontend::WebContentsDestroyed() {
agent_host_->DetachClient(this);
agent_host_ = nullptr;
}
std::move(frontend_destroyed_callback_).Run();
delete this;
}
@@ -300,6 +380,9 @@ void CefDevToolsFrontend::HandleMessageFromDevToolsFrontend(
std::string protocol_message;
if (!agent_host_ || !params->GetString(0, &protocol_message))
return;
if (ProtocolLoggingEnabled()) {
LogProtocolMessage(ProtocolMessageType::METHOD, protocol_message);
}
agent_host_->DispatchProtocolMessage(
this, base::as_bytes(base::make_span(protocol_message)));
} else if (method == "loadCompleted") {
@@ -451,6 +534,14 @@ void CefDevToolsFrontend::DispatchProtocolMessage(
base::span<const uint8_t> message) {
base::StringPiece str_message(reinterpret_cast<const char*>(message.data()),
message.size());
if (ProtocolLoggingEnabled()) {
// Quick check to avoid parsing the JSON object. Events begin with a
// "method" value whereas method results begin with an "id" value.
LogProtocolMessage(str_message.starts_with("{\"method\":")
? ProtocolMessageType::EVENT
: ProtocolMessageType::RESULT,
str_message);
}
if (str_message.length() < kMaxMessageChunkSize) {
std::string param;
base::EscapeJSONString(str_message, true, &param);
@@ -504,6 +595,23 @@ void CefDevToolsFrontend::SendMessageAck(int request_id,
CallClientFunction("DevToolsAPI.embedderMessageAck", &id_value, arg, nullptr);
}
bool CefDevToolsFrontend::ProtocolLoggingEnabled() const {
return !protocol_log_file_.empty();
}
void CefDevToolsFrontend::LogProtocolMessage(ProtocolMessageType type,
const base::StringPiece& message) {
DCHECK(ProtocolLoggingEnabled());
std::string to_log = message.substr(0, kMaxLogLineLength).as_string();
// Execute in an ordered context that allows blocking.
auto task_runner = CefContentBrowserClient::Get()->background_task_runner();
task_runner->PostTask(
FROM_HERE, base::BindOnce(::LogProtocolMessage, protocol_log_file_, type,
std::move(to_log)));
}
void CefDevToolsFrontend::AgentHostClosed(
content::DevToolsAgentHost* agent_host) {
DCHECK(agent_host == agent_host_.get());

View File

@@ -11,6 +11,7 @@
#include "libcef/browser/devtools/devtools_file_manager.h"
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
@@ -31,36 +32,38 @@ class WebContents;
class PrefService;
enum class ProtocolMessageType {
METHOD,
RESULT,
EVENT,
};
class CefDevToolsFrontend : public content::WebContentsObserver,
public content::DevToolsAgentHostClient {
public:
static CefDevToolsFrontend* Show(
CefRefPtr<CefBrowserHostImpl> inspected_browser,
CefBrowserHostImpl* inspected_browser,
const CefWindowInfo& windowInfo,
CefRefPtr<CefClient> client,
const CefBrowserSettings& settings,
const CefPoint& inspect_element_at);
const CefPoint& inspect_element_at,
base::OnceClosure frontend_destroyed_callback);
void Activate();
void Focus();
void InspectElementAt(int x, int y);
void Close();
void DisconnectFromTarget();
void CallClientFunction(const std::string& function_name,
const base::Value* arg1,
const base::Value* arg2,
const base::Value* arg3);
CefRefPtr<CefBrowserHostImpl> frontend_browser() const {
return frontend_browser_;
}
private:
CefDevToolsFrontend(CefRefPtr<CefBrowserHostImpl> frontend_browser,
CefDevToolsFrontend(CefBrowserHostImpl* frontend_browser,
content::WebContents* inspected_contents,
const CefPoint& inspect_element_at);
const CefPoint& inspect_element_at,
base::OnceClosure destroyed_callback);
~CefDevToolsFrontend() override;
// content::DevToolsAgentHostClient implementation.
@@ -78,12 +81,17 @@ class CefDevToolsFrontend : public content::WebContentsObserver,
void SendMessageAck(int request_id, const base::Value* arg1);
bool ProtocolLoggingEnabled() const;
void LogProtocolMessage(ProtocolMessageType type,
const base::StringPiece& message);
PrefService* GetPrefs() const;
CefRefPtr<CefBrowserHostImpl> frontend_browser_;
content::WebContents* inspected_contents_;
scoped_refptr<content::DevToolsAgentHost> agent_host_;
CefPoint inspect_element_at_;
base::OnceClosure frontend_destroyed_callback_;
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;
class NetworkResourceLoader;
@@ -93,6 +101,9 @@ class CefDevToolsFrontend : public content::WebContentsObserver,
using ExtensionsAPIs = std::map<std::string, std::string>;
ExtensionsAPIs extensions_api_;
CefDevToolsFileManager file_manager_;
const base::FilePath protocol_log_file_;
base::WeakPtrFactory<CefDevToolsFrontend> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(CefDevToolsFrontend);

View File

@@ -0,0 +1,200 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 "libcef/browser/devtools/devtools_manager.h"
#include "libcef/browser/devtools/devtools_controller.h"
#include "libcef/browser/devtools/devtools_frontend.h"
#include "content/public/browser/web_contents.h"
namespace {
// May be created on any thread but will be destroyed on the UI thread.
class CefDevToolsRegistrationImpl : public CefRegistration,
public CefDevToolsController::Observer {
public:
explicit CefDevToolsRegistrationImpl(
CefRefPtr<CefDevToolsMessageObserver> observer)
: observer_(observer) {
DCHECK(observer_);
}
~CefDevToolsRegistrationImpl() override {
CEF_REQUIRE_UIT();
// May be null if OnDevToolsControllerDestroyed was called.
if (!controller_)
return;
controller_->RemoveObserver(this);
}
void Initialize(CefBrowserHostImpl* browser,
base::WeakPtr<CefDevToolsController> controller) {
CEF_REQUIRE_UIT();
DCHECK(browser && controller);
DCHECK(!browser_ && !controller_);
browser_ = browser;
controller_ = controller;
controller_->AddObserver(this);
}
private:
// CefDevToolsController::Observer methods:
bool OnDevToolsMessage(const base::StringPiece& message) override {
CEF_REQUIRE_UIT();
return observer_->OnDevToolsMessage(browser_, message.data(),
message.size());
}
void OnDevToolsMethodResult(int message_id,
bool success,
const base::StringPiece& result) override {
CEF_REQUIRE_UIT();
observer_->OnDevToolsMethodResult(browser_, message_id, success,
result.data(), result.size());
}
void OnDevToolsEvent(const base::StringPiece& method,
const base::StringPiece& params) override {
CEF_REQUIRE_UIT();
observer_->OnDevToolsEvent(browser_, method.as_string(), params.data(),
params.size());
}
void OnDevToolsAgentAttached() override {
CEF_REQUIRE_UIT();
observer_->OnDevToolsAgentAttached(browser_);
}
void OnDevToolsAgentDetached() override {
CEF_REQUIRE_UIT();
observer_->OnDevToolsAgentDetached(browser_);
}
void OnDevToolsControllerDestroyed() override {
CEF_REQUIRE_UIT();
browser_ = nullptr;
controller_.reset();
}
CefRefPtr<CefDevToolsMessageObserver> observer_;
CefBrowserHostImpl* browser_ = nullptr;
base::WeakPtr<CefDevToolsController> controller_;
IMPLEMENT_REFCOUNTING_DELETE_ON_UIT(CefDevToolsRegistrationImpl);
DISALLOW_COPY_AND_ASSIGN(CefDevToolsRegistrationImpl);
};
} // namespace
CefDevToolsManager::CefDevToolsManager(CefBrowserHostImpl* inspected_browser)
: inspected_browser_(inspected_browser), weak_ptr_factory_(this) {
CEF_REQUIRE_UIT();
}
CefDevToolsManager::~CefDevToolsManager() {
CEF_REQUIRE_UIT();
}
void CefDevToolsManager::ShowDevTools(const CefWindowInfo& windowInfo,
CefRefPtr<CefClient> client,
const CefBrowserSettings& settings,
const CefPoint& inspect_element_at) {
CEF_REQUIRE_UIT();
if (devtools_frontend_) {
if (!inspect_element_at.IsEmpty()) {
devtools_frontend_->InspectElementAt(inspect_element_at.x,
inspect_element_at.y);
}
devtools_frontend_->Focus();
return;
}
devtools_frontend_ = CefDevToolsFrontend::Show(
inspected_browser_, windowInfo, client, settings, inspect_element_at,
base::BindOnce(&CefDevToolsManager::OnFrontEndDestroyed,
weak_ptr_factory_.GetWeakPtr()));
}
void CefDevToolsManager::CloseDevTools() {
CEF_REQUIRE_UIT();
if (!devtools_frontend_)
return;
devtools_frontend_->Close();
}
bool CefDevToolsManager::HasDevTools() {
CEF_REQUIRE_UIT();
return !!devtools_frontend_;
}
bool CefDevToolsManager::SendDevToolsMessage(const void* message,
size_t message_size) {
CEF_REQUIRE_UIT();
if (!message || message_size == 0)
return false;
if (!EnsureController())
return false;
return devtools_controller_->SendDevToolsMessage(
base::StringPiece(static_cast<const char*>(message), message_size));
}
int CefDevToolsManager::ExecuteDevToolsMethod(
int message_id,
const CefString& method,
CefRefPtr<CefDictionaryValue> params) {
CEF_REQUIRE_UIT();
if (method.empty())
return 0;
if (!EnsureController())
return 0;
if (params && params->IsValid()) {
CefDictionaryValueImpl* impl =
static_cast<CefDictionaryValueImpl*>(params.get());
CefValueController::AutoLock lock_scope(impl->controller());
return devtools_controller_->ExecuteDevToolsMethod(message_id, method,
impl->GetValueUnsafe());
} else {
return devtools_controller_->ExecuteDevToolsMethod(message_id, method,
nullptr);
}
}
// static
CefRefPtr<CefRegistration> CefDevToolsManager::CreateRegistration(
CefRefPtr<CefDevToolsMessageObserver> observer) {
DCHECK(observer);
return new CefDevToolsRegistrationImpl(observer);
}
void CefDevToolsManager::InitializeRegistrationOnUIThread(
CefRefPtr<CefRegistration> registration) {
CEF_REQUIRE_UIT();
if (!EnsureController())
return;
static_cast<CefDevToolsRegistrationImpl*>(registration.get())
->Initialize(inspected_browser_, devtools_controller_->GetWeakPtr());
}
void CefDevToolsManager::OnFrontEndDestroyed() {
devtools_frontend_ = nullptr;
}
bool CefDevToolsManager::EnsureController() {
if (!devtools_controller_) {
devtools_controller_.reset(
new CefDevToolsController(inspected_browser_->web_contents()));
}
return true;
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_MANAGER_H_
#define CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_MANAGER_H_
#pragma once
#include "include/cef_browser.h"
#include "base/memory/weak_ptr.h"
class CefBrowserHostImpl;
class CefDevToolsController;
class CefDevToolsFrontend;
namespace content {
class WebContents;
}
// Manages DevTools instances. Methods must be called on the UI thread unless
// otherwise indicated.
class CefDevToolsManager {
public:
// |inspected_browser| will outlive this object.
explicit CefDevToolsManager(CefBrowserHostImpl* inspected_browser);
~CefDevToolsManager();
// See CefBrowserHost methods of the same name for documentation.
void ShowDevTools(const CefWindowInfo& windowInfo,
CefRefPtr<CefClient> client,
const CefBrowserSettings& settings,
const CefPoint& inspect_element_at);
void CloseDevTools();
bool HasDevTools();
bool SendDevToolsMessage(const void* message, size_t message_size);
int ExecuteDevToolsMethod(int message_id,
const CefString& method,
CefRefPtr<CefDictionaryValue> param);
// These methods are used to implement
// CefBrowserHost::AddDevToolsMessageObserver. CreateRegistration is safe to
// call on any thread. InitializeRegistrationOnUIThread should be called
// immediately afterwards on the UI thread.
static CefRefPtr<CefRegistration> CreateRegistration(
CefRefPtr<CefDevToolsMessageObserver> observer);
void InitializeRegistrationOnUIThread(
CefRefPtr<CefRegistration> registration);
private:
void OnFrontEndDestroyed();
bool EnsureController();
CefBrowserHostImpl* const inspected_browser_;
// CefDevToolsFrontend will delete itself when the frontend WebContents is
// destroyed.
CefDevToolsFrontend* devtools_frontend_ = nullptr;
// Used for sending DevTools protocol messages without an active frontend.
std::unique_ptr<CefDevToolsController> devtools_controller_;
base::WeakPtrFactory<CefDevToolsManager> weak_ptr_factory_;
DISALLOW_COPY_AND_ASSIGN(CefDevToolsManager);
};
#endif // CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_MANAGER_H_

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 "libcef/browser/devtools/devtools_util.h"
#include "base/strings/string_number_conversions.h"
namespace devtools_util {
namespace {
bool IsValidDictionary(const base::StringPiece& str, bool allow_empty) {
return str.length() >= (allow_empty ? 2 : 3) && str[0] == '{' &&
str[str.length() - 1] == '}';
}
// Example:
// {"method":"Target.targetDestroyed","params":{"targetId":"1234..."}}
bool ParseEvent(const base::StringPiece& message,
base::StringPiece& method,
base::StringPiece& params) {
static const char kMethodStart[] = "{\"method\":\"";
static const char kMethodEnd[] = "\"";
static const char kParamsStart[] = ",\"params\":";
if (!message.starts_with(kMethodStart))
return false;
const size_t method_start = sizeof(kMethodStart) - 1;
const size_t method_end = message.find(kMethodEnd, method_start);
if (method_end < 0U)
return false;
method = message.substr(method_start, method_end - method_start);
if (method.empty())
return false;
size_t remainder_start = method_end + sizeof(kMethodEnd) - 1;
if (remainder_start == message.size() - 1) {
// No more contents.
params = base::StringPiece();
} else {
const base::StringPiece& remainder = message.substr(remainder_start);
if (remainder.starts_with(kParamsStart)) {
// Stop immediately before the message closing bracket.
remainder_start += sizeof(kParamsStart) - 1;
params =
message.substr(remainder_start, message.size() - 1 - remainder_start);
} else {
// Invalid format.
return false;
}
if (!IsValidDictionary(params, /*allow_empty=*/true))
return false;
}
return true;
}
// Examples:
// {"id":3,"result":{}}
// {"id":4,"result":{"debuggerId":"-2193881606781505058.81393575456727957"}}
// {"id":5,"error":{"code":-32000,"message":"Not supported"}}
bool ParseResult(const base::StringPiece& message,
int& message_id,
bool& success,
base::StringPiece& result) {
static const char kIdStart[] = "{\"id\":";
static const char kIdEnd[] = ",";
static const char kResultStart[] = "\"result\":";
static const char kErrorStart[] = "\"error\":";
if (!message.starts_with(kIdStart))
return false;
const size_t id_start = sizeof(kIdStart) - 1;
const size_t id_end = message.find(kIdEnd, id_start);
if (id_end < 0U)
return false;
const base::StringPiece& id_str = message.substr(id_start, id_end - id_start);
if (id_str.empty() || !base::StringToInt(id_str, &message_id))
return false;
size_t remainder_start = id_end + sizeof(kIdEnd) - 1;
const base::StringPiece& remainder = message.substr(remainder_start);
if (remainder.starts_with(kResultStart)) {
// Stop immediately before the message closing bracket.
remainder_start += sizeof(kResultStart) - 1;
result =
message.substr(remainder_start, message.size() - 1 - remainder_start);
success = true;
} else if (remainder.starts_with(kErrorStart)) {
// Stop immediately before the message closing bracket.
remainder_start += sizeof(kErrorStart) - 1;
result =
message.substr(remainder_start, message.size() - 1 - remainder_start);
success = false;
} else {
// Invalid format.
return false;
}
if (!IsValidDictionary(result, /*allow_empty=*/true))
return false;
return true;
}
} // namespace
// static
bool ProtocolParser::IsValidMessage(const base::StringPiece& message) {
return IsValidDictionary(message, /*allow_empty=*/false);
}
bool ProtocolParser::Initialize(const base::StringPiece& message) {
if (status_ != UNINITIALIZED)
return false;
if (ParseEvent(message, method_, params_)) {
status_ = EVENT;
} else if (ParseResult(message, message_id_, success_, params_)) {
status_ = RESULT;
} else {
status_ = FAILURE;
}
return true;
}
} // namespace devtools_util

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_UTIL_H_
#define CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_UTIL_H_
#pragma once
#include "base/strings/string_piece.h"
namespace devtools_util {
// Fast parser for DevTools JSON protocol messages. This implementation makes
// certain assumptions about the JSON object structure (value order and
// formatting) to avoid stateful parsing of messages that may be large
// (sometimes > 1MB in size). The message must be a JSON dictionary that starts
// with a "method" or "id" value which is non-empty and of the expected data
// type. Messages that have a "method" value (event message) may optionally have
// a "params" dictionary. Messages that have an "id" value (result message) must
// have a "result" or "error" dictionary. The dictionary contents are not
// validated and may be empty ("{}").
//
// Example event message:
// {"method":"Target.targetDestroyed","params":{"targetId":"1234..."}}
//
// Example result messages:
// {"id":3,"result":{}}
// {"id":4,"result":{"debuggerId":"-2193881606781505058.81393575456727957"}}
// {"id":5,"error":{"code":-32000,"message":"Not supported"}}
struct ProtocolParser {
ProtocolParser() = default;
// Checks for a non-empty JSON dictionary.
static bool IsValidMessage(const base::StringPiece& message);
// Returns false if already initialized.
bool Initialize(const base::StringPiece& message);
bool IsInitialized() const { return status_ != UNINITIALIZED; }
bool IsEvent() const { return status_ == EVENT; }
bool IsResult() const { return status_ == RESULT; }
bool IsFailure() const { return status_ == FAILURE; }
void Reset() { status_ = UNINITIALIZED; }
// For event messages:
// "method" string:
base::StringPiece method_;
// For result messages:
// "id" int:
int message_id_ = 0;
// true if "result" value, false if "error" value:
bool success_ = false;
// For both:
// "params", "result" or "error" dictionary:
base::StringPiece params_;
private:
enum Status {
UNINITIALIZED,
EVENT, // Event message.
RESULT, // Result message.
FAILURE, // Parsing failure.
};
Status status_ = UNINITIALIZED;
};
} // namespace devtools_util
#endif // CEF_LIBCEF_BROWSER_DEVTOOLS_DEVTOOLS_UTIL_H_

View File

@@ -0,0 +1,204 @@
// Copyright (c) 2020 The Chromium Embedded Framework 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 "libcef/browser/devtools/devtools_util.h"
#include "testing/gtest/include/gtest/gtest.h"
using namespace devtools_util;
TEST(DevToolsUtil, ProtocolParser_IsValidMessage) {
// Empty dictionary is not valid.
EXPECT_FALSE(ProtocolParser::IsValidMessage(""));
EXPECT_FALSE(ProtocolParser::IsValidMessage("{}"));
// Incorrectly formatted dictionary is not valid.
EXPECT_FALSE(ProtocolParser::IsValidMessage("{ ]"));
// Everything else is valid (we don't verify JSON structure).
EXPECT_TRUE(ProtocolParser::IsValidMessage("{ }"));
EXPECT_TRUE(ProtocolParser::IsValidMessage("{blah blah}"));
EXPECT_TRUE(ProtocolParser::IsValidMessage("{method:\"foobar\"}"));
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsFailure_Unknown) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Empty message is invalid.
EXPECT_TRUE(parser.Initialize(""));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Empty dictionary is invalid.
EXPECT_TRUE(parser.Initialize("{}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Unrecognized dictionary type is invalid.
EXPECT_TRUE(parser.Initialize("{blah blah}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsFailure_EventMalformed) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Empty method is invalid.
EXPECT_TRUE(parser.Initialize("{\"method\":\"\"}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Unrecognized value is invalid.
EXPECT_TRUE(parser.Initialize("{\"method\":\"foo\",oops:false}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Params must be a dictionary.
EXPECT_TRUE(parser.Initialize("{\"method\":\",params:[]}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsEvent) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Method without params is valid.
std::string message = "{\"method\":\"Test.myMethod\"}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsEvent());
EXPECT_STREQ("Test.myMethod", parser.method_.as_string().data());
EXPECT_TRUE(parser.params_.empty());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Method with empty params dictionary is valid.
message = "{\"method\":\"Test.myMethod2\",\"params\":{}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsEvent());
EXPECT_STREQ("Test.myMethod2", parser.method_.as_string().data());
EXPECT_STREQ("{}", parser.params_.as_string().data());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Method with non-empty params dictionary is valid.
message = "{\"method\":\"Test.myMethod3\",\"params\":{\"foo\":\"bar\"}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsEvent());
EXPECT_STREQ("Test.myMethod3", parser.method_.as_string().data());
EXPECT_STREQ("{\"foo\":\"bar\"}", parser.params_.as_string().data());
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsFailure_ResultMalformed) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Empty ID is invalid.
EXPECT_TRUE(parser.Initialize("{\"id\":,result:{}}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Missing result or error value is invalid.
EXPECT_TRUE(parser.Initialize("{\"id\":1}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Unrecognized value is invalid.
EXPECT_TRUE(parser.Initialize("{\"id\":1,oops:false}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Result must be a dictionary.
EXPECT_TRUE(parser.Initialize("{\"id\":1,\"result\":[]}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Error must be a dictionary.
EXPECT_TRUE(parser.Initialize("{\"id\":1,\"error\":[]}"));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsFailure());
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsResult_Result) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Id with empty result dictionary is valid.
std::string message = "{\"id\":1,\"result\":{}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsResult());
EXPECT_EQ(1, parser.message_id_);
EXPECT_TRUE(parser.success_);
EXPECT_STREQ("{}", parser.params_.as_string().data());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Id with non-empty result dictionary is valid.
message = "{\"id\":2,\"result\":{\"foo\":\"bar\"}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsResult());
EXPECT_EQ(2, parser.message_id_);
EXPECT_TRUE(parser.success_);
EXPECT_STREQ("{\"foo\":\"bar\"}", parser.params_.as_string().data());
}
TEST(DevToolsUtil, ProtocolParser_Initialize_IsResult_Error) {
ProtocolParser parser;
EXPECT_FALSE(parser.IsInitialized());
// Id with empty error dictionary is valid.
std::string message = "{\"id\":1,\"error\":{}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsResult());
EXPECT_EQ(1, parser.message_id_);
EXPECT_FALSE(parser.success_);
EXPECT_STREQ("{}", parser.params_.as_string().data());
parser.Reset();
EXPECT_FALSE(parser.IsInitialized());
// Id with non-empty error dictionary is valid.
message = "{\"id\":2,\"error\":{\"foo\":\"bar\"}}";
EXPECT_TRUE(parser.Initialize(message));
EXPECT_TRUE(parser.IsInitialized());
EXPECT_TRUE(parser.IsResult());
EXPECT_EQ(2, parser.message_id_);
EXPECT_FALSE(parser.success_);
EXPECT_STREQ("{\"foo\":\"bar\"}", parser.params_.as_string().data());
}