Merge pull request #5711 from electron/extension-code-cleanup

Implement partial chrome.* API for devtools extension
This commit is contained in:
Cheng Zhao 2016-05-29 23:29:08 +00:00
commit 9f0fc96025
32 changed files with 1151 additions and 216 deletions

View file

@ -0,0 +1,88 @@
// 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_render_process_preferences.h"
#include "atom/browser/atom_browser_client.h"
#include "atom/browser/native_window.h"
#include "atom/browser/window_list.h"
#include "atom/common/native_mate_converters/value_converter.h"
#include "atom/common/node_includes.h"
#include "content/public/browser/render_process_host.h"
#include "native_mate/dictionary.h"
#include "native_mate/object_template_builder.h"
namespace atom {
namespace api {
namespace {
bool IsBrowserWindow(content::RenderProcessHost* process) {
content::WebContents* web_contents =
static_cast<AtomBrowserClient*>(AtomBrowserClient::Get())->
GetWebContentsFromProcessID(process->GetID());
if (!web_contents)
return false;
NativeWindow* window = NativeWindow::FromWebContents(web_contents);
if (!window)
return false;
return true;
}
} // namespace
RenderProcessPreferences::RenderProcessPreferences(
v8::Isolate* isolate,
const atom::RenderProcessPreferences::Predicate& predicate)
: preferences_(predicate) {
Init(isolate);
}
RenderProcessPreferences::~RenderProcessPreferences() {
}
int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) {
return preferences_.AddEntry(entry);
}
void RenderProcessPreferences::RemoveEntry(int id) {
preferences_.RemoveEntry(id);
}
// static
void RenderProcessPreferences::BuildPrototype(
v8::Isolate* isolate, v8::Local<v8::ObjectTemplate> prototype) {
mate::ObjectTemplateBuilder(isolate, prototype)
.SetMethod("addEntry", &RenderProcessPreferences::AddEntry)
.SetMethod("removeEntry", &RenderProcessPreferences::RemoveEntry);
}
// static
mate::Handle<RenderProcessPreferences>
RenderProcessPreferences::ForAllBrowserWindow(v8::Isolate* isolate) {
return mate::CreateHandle(
isolate,
new RenderProcessPreferences(isolate, base::Bind(&IsBrowserWindow)));
}
} // namespace api
} // namespace atom
namespace {
void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused,
v8::Local<v8::Context> context, void* priv) {
mate::Dictionary dict(context->GetIsolate(), exports);
dict.SetMethod("forAllBrowserWindow",
&atom::api::RenderProcessPreferences::ForAllBrowserWindow);
}
} // namespace
NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_render_process_preferences,
Initialize)

View file

@ -0,0 +1,44 @@
// 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_RENDER_PROCESS_PREFERENCES_H_
#define ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_
#include "atom/browser/render_process_preferences.h"
#include "native_mate/handle.h"
#include "native_mate/wrappable.h"
namespace atom {
namespace api {
class RenderProcessPreferences
: public mate::Wrappable<RenderProcessPreferences> {
public:
static mate::Handle<RenderProcessPreferences>
ForAllBrowserWindow(v8::Isolate* isolate);
static void BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> prototype);
int AddEntry(const base::DictionaryValue& entry);
void RemoveEntry(int id);
protected:
RenderProcessPreferences(
v8::Isolate* isolate,
const atom::RenderProcessPreferences::Predicate& predicate);
~RenderProcessPreferences() override;
private:
atom::RenderProcessPreferences preferences_;
DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences);
};
} // namespace api
} // namespace atom
#endif // ATOM_BROWSER_API_ATOM_API_RENDER_PROCESS_PREFERENCES_H_

View file

@ -655,6 +655,11 @@ void WebContents::DevToolsOpened() {
isolate(), managed_web_contents()->GetDevToolsWebContents());
devtools_web_contents_.Reset(isolate(), handle.ToV8());
// Set inspected tabID.
base::FundamentalValue tab_id(ID());
managed_web_contents()->CallClientFunction(
"DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr);
// Inherit owner window in devtools.
if (owner_window())
handle->SetOwnerWindow(managed_web_contents()->GetDevToolsWebContents(),
@ -1083,9 +1088,10 @@ void WebContents::TabTraverse(bool reverse) {
web_contents()->FocusThroughTabTraversal(reverse);
}
bool WebContents::SendIPCMessage(const base::string16& channel,
bool WebContents::SendIPCMessage(bool all_frames,
const base::string16& channel,
const base::ListValue& args) {
return Send(new AtomViewMsg_Message(routing_id(), channel, args));
return Send(new AtomViewMsg_Message(routing_id(), all_frames, channel, args));
}
void WebContents::SendInputEvent(v8::Isolate* isolate,
@ -1333,6 +1339,8 @@ void Initialize(v8::Local<v8::Object> exports, v8::Local<v8::Value> unused,
mate::Dictionary dict(isolate, exports);
dict.SetMethod("create", &atom::api::WebContents::Create);
dict.SetMethod("_setWrapWebContents", &atom::api::SetWrapWebContents);
dict.SetMethod("fromId",
&mate::TrackableObject<atom::api::WebContents>::FromWeakMapID);
}
} // namespace

View file

@ -122,7 +122,8 @@ class WebContents : public mate::TrackableObject<WebContents>,
void TabTraverse(bool reverse);
// Send messages to browser.
bool SendIPCMessage(const base::string16& channel,
bool SendIPCMessage(bool all_frames,
const base::string16& channel,
const base::ListValue& args);
// Send WebInputEvent to the page.

View file

@ -73,6 +73,17 @@ AtomBrowserClient::AtomBrowserClient() : delegate_(nullptr) {
AtomBrowserClient::~AtomBrowserClient() {
}
content::WebContents* AtomBrowserClient::GetWebContentsFromProcessID(
int process_id) {
// If the process is a pending process, we should use the old one.
if (ContainsKey(pending_processes_, process_id))
process_id = pending_processes_[process_id];
// Certain render process will be created with no associated render view,
// for example: ServiceWorker.
return WebContentsPreferences::GetWebContentsFromProcessID(process_id);
}
void AtomBrowserClient::RenderProcessWillLaunch(
content::RenderProcessHost* host) {
int process_id = host->GetID();
@ -172,14 +183,7 @@ void AtomBrowserClient::AppendExtraCommandLineSwitches(
}
#endif
// If the process is a pending process, we should use the old one.
if (ContainsKey(pending_processes_, process_id))
process_id = pending_processes_[process_id];
// Certain render process will be created with no associated render view,
// for example: ServiceWorker.
content::WebContents* web_contents =
WebContentsPreferences::GetWebContentsFromProcessID(process_id);
content::WebContents* web_contents = GetWebContentsFromProcessID(process_id);
if (!web_contents)
return;

View file

@ -34,6 +34,9 @@ class AtomBrowserClient : public brightray::BrowserClient,
using Delegate = content::ContentBrowserClient;
void set_delegate(Delegate* delegate) { delegate_ = delegate; }
// Returns the WebContents for pending render processes.
content::WebContents* GetWebContentsFromProcessID(int process_id);
// Don't force renderer process to restart for once.
static void SuppressRendererProcessRestartForOnce();

View file

@ -8,10 +8,24 @@
#include "atom/common/atom_constants.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "net/base/mime_util.h"
#include "net/base/net_errors.h"
namespace atom {
namespace {
std::string GetExtFromURL(const GURL& url) {
std::string spec = url.spec();
size_t index = spec.find_last_of('.');
if (index == std::string::npos || index == spec.size())
return std::string();
return spec.substr(index + 1, spec.size() - index - 1);
}
} // namespace
URLRequestBufferJob::URLRequestBufferJob(
net::URLRequest* request, net::NetworkDelegate* network_delegate)
: JsAsker<net::URLRequestSimpleJob>(request, network_delegate),
@ -30,6 +44,15 @@ void URLRequestBufferJob::StartAsync(std::unique_ptr<base::Value> options) {
options->GetAsBinary(&binary);
}
if (mime_type_.empty()) {
std::string ext = GetExtFromURL(request()->url());
#if defined(OS_WIN)
net::GetWellKnownMimeTypeFromExtension(base::UTF8ToUTF16(ext), &mime_type_);
#else
net::GetWellKnownMimeTypeFromExtension(ext, &mime_type_);
#endif
}
if (!binary) {
NotifyStartError(net::URLRequestStatus(
net::URLRequestStatus::FAILED, net::ERR_NOT_IMPLEMENTED));

View file

@ -0,0 +1,63 @@
// 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/render_process_preferences.h"
#include "atom/common/api/api_messages.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_process_host.h"
namespace atom {
RenderProcessPreferences::RenderProcessPreferences(const Predicate& predicate)
: predicate_(predicate),
next_id_(0),
cache_needs_update_(true) {
registrar_.Add(this,
content::NOTIFICATION_RENDERER_PROCESS_CREATED,
content::NotificationService::AllBrowserContextsAndSources());
}
RenderProcessPreferences::~RenderProcessPreferences() {
}
int RenderProcessPreferences::AddEntry(const base::DictionaryValue& entry) {
int id = ++next_id_;
entries_[id] = entry.CreateDeepCopy();
cache_needs_update_ = true;
return id;
}
void RenderProcessPreferences::RemoveEntry(int id) {
cache_needs_update_ = true;
entries_.erase(id);
}
void RenderProcessPreferences::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(type, content::NOTIFICATION_RENDERER_PROCESS_CREATED);
content::RenderProcessHost* process =
content::Source<content::RenderProcessHost>(source).ptr();
if (!predicate_.Run(process))
return;
UpdateCache();
process->Send(new AtomMsg_UpdatePreferences(cached_entries_));
}
void RenderProcessPreferences::UpdateCache() {
if (!cache_needs_update_)
return;
cached_entries_.Clear();
for (const auto& iter : entries_)
cached_entries_.Append(iter.second->CreateDeepCopy());
cache_needs_update_ = false;
}
} // namespace atom

View file

@ -0,0 +1,61 @@
// 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_RENDER_PROCESS_PREFERENCES_H_
#define ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_
#include <memory>
#include <unordered_map>
#include "base/callback.h"
#include "base/values.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
namespace content {
class RenderProcessHost;
}
namespace atom {
// Sets user preferences for render processes.
class RenderProcessPreferences : public content::NotificationObserver {
public:
using Predicate = base::Callback<bool(content::RenderProcessHost*)>;
// The |predicate| is used to determine whether to set preferences for a
// render process.
explicit RenderProcessPreferences(const Predicate& predicate);
virtual ~RenderProcessPreferences();
int AddEntry(const base::DictionaryValue& entry);
void RemoveEntry(int id);
private:
// content::NotificationObserver:
void Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) override;
void UpdateCache();
// Manages our notification registrations.
content::NotificationRegistrar registrar_;
Predicate predicate_;
int next_id_;
std::unordered_map<int, std::unique_ptr<base::DictionaryValue>> entries_;
// We need to convert the |entries_| to ListValue for multiple times, this
// caches is only updated when we are sending messages.
bool cache_needs_update_;
base::ListValue cached_entries_;
DISALLOW_COPY_AND_ASSIGN(RenderProcessPreferences);
};
} // namespace atom
#endif // ATOM_BROWSER_RENDER_PROCESS_PREFERENCES_H_

View file

@ -150,6 +150,16 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
command_line->AppendSwitch(switches::kScrollBounce);
#endif
// Custom command line switches.
const base::ListValue* args;
if (web_preferences.GetList("commandLineSwitches", &args)) {
for (size_t i = 0; i < args->GetSize(); ++i) {
std::string arg;
if (args->GetString(i, &arg) && !arg.empty())
command_line->AppendSwitch(arg);
}
}
// Enable blink features.
std::string blink_features;
if (web_preferences.GetString(options::kBlinkFeatures, &blink_features))

View file

@ -29,6 +29,7 @@ class WebContentsPreferences
: public content::WebContentsUserData<WebContentsPreferences> {
public:
// Get WebContents according to process ID.
// FIXME(zcbenz): This method does not belong here.
static content::WebContents* GetWebContentsFromProcessID(int process_id);
// Append command paramters according to |web_contents|'s preferences.

View file

@ -30,10 +30,14 @@ IPC_SYNC_MESSAGE_ROUTED2_1(AtomViewHostMsg_Message_Sync,
base::ListValue /* arguments */,
base::string16 /* result (in JSON) */)
IPC_MESSAGE_ROUTED2(AtomViewMsg_Message,
IPC_MESSAGE_ROUTED3(AtomViewMsg_Message,
bool /* send_to_all */,
base::string16 /* channel */,
base::ListValue /* arguments */)
// Sent by the renderer when the draggable regions are updated.
IPC_MESSAGE_ROUTED1(AtomViewHostMsg_UpdateDraggableRegions,
std::vector<atom::DraggableRegion> /* regions */)
// Update renderer process preferences.
IPC_MESSAGE_CONTROL1(AtomMsg_UpdatePreferences, base::ListValue)

View file

@ -35,7 +35,7 @@ void RemoteCallbackFreer::RunDestructor() {
base::ASCIIToUTF16("ELECTRON_RENDERER_RELEASE_CALLBACK");
base::ListValue args;
args.AppendInteger(object_id_);
Send(new AtomViewMsg_Message(routing_id(), channel, args));
Send(new AtomViewMsg_Message(routing_id(), false, channel, args));
Observe(nullptr);
}

View file

@ -42,6 +42,7 @@ REFERENCE_MODULE(atom_browser_power_monitor);
REFERENCE_MODULE(atom_browser_power_save_blocker);
REFERENCE_MODULE(atom_browser_protocol);
REFERENCE_MODULE(atom_browser_global_shortcut);
REFERENCE_MODULE(atom_browser_render_process_preferences);
REFERENCE_MODULE(atom_browser_session);
REFERENCE_MODULE(atom_browser_system_preferences);
REFERENCE_MODULE(atom_browser_tray);

View file

@ -59,6 +59,34 @@ std::vector<v8::Local<v8::Value>> ListValueToVector(
return result;
}
void EmitIPCEvent(blink::WebFrame* frame,
const base::string16& channel,
const base::ListValue& args) {
if (!frame || frame->isWebRemoteFrame())
return;
v8::Isolate* isolate = blink::mainThreadIsolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = frame->mainWorldScriptContext();
v8::Context::Scope context_scope(context);
// Only emit IPC event for context with node integration.
node::Environment* env = node::Environment::GetCurrent(context);
if (!env)
return;
v8::Local<v8::Object> ipc;
if (GetIPCObject(isolate, context, &ipc)) {
auto args_vector = ListValueToVector(isolate, args);
// Insert the Event object, event.sender is ipc.
mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate);
event.Set("sender", ipc);
args_vector.insert(args_vector.begin(), event.GetHandle());
mate::EmitEvent(isolate, ipc, channel, args_vector);
}
}
base::StringPiece NetResourceProvider(int key) {
if (key == IDR_DIR_HEADER_HTML) {
base::StringPiece html_data =
@ -123,7 +151,8 @@ bool AtomRenderViewObserver::OnMessageReceived(const IPC::Message& message) {
return handled;
}
void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel,
void AtomRenderViewObserver::OnBrowserMessage(bool send_to_all,
const base::string16& channel,
const base::ListValue& args) {
if (!document_created_)
return;
@ -135,20 +164,13 @@ void AtomRenderViewObserver::OnBrowserMessage(const base::string16& channel,
if (!frame || frame->isWebRemoteFrame())
return;
v8::Isolate* isolate = blink::mainThreadIsolate();
v8::HandleScope handle_scope(isolate);
EmitIPCEvent(frame, channel, args);
v8::Local<v8::Context> context = frame->mainWorldScriptContext();
v8::Context::Scope context_scope(context);
v8::Local<v8::Object> ipc;
if (GetIPCObject(isolate, context, &ipc)) {
auto args_vector = ListValueToVector(isolate, args);
// Insert the Event object, event.sender is ipc.
mate::Dictionary event = mate::Dictionary::CreateEmpty(isolate);
event.Set("sender", ipc);
args_vector.insert(args_vector.begin(), event.GetHandle());
mate::EmitEvent(isolate, ipc, channel, args_vector);
// Also send the message to all sub-frames.
if (send_to_all) {
for (blink::WebFrame* child = frame->firstChild(); child;
child = child->nextSibling())
EmitIPCEvent(child, channel, args);
}
}

View file

@ -30,7 +30,8 @@ class AtomRenderViewObserver : public content::RenderViewObserver {
void DraggableRegionsChanged(blink::WebFrame* frame) override;
bool OnMessageReceived(const IPC::Message& message) override;
void OnBrowserMessage(const base::string16& channel,
void OnBrowserMessage(bool send_to_all,
const base::string16& channel,
const base::ListValue& args);
// Weak reference to renderer client.

View file

@ -11,12 +11,14 @@
#include "atom/common/api/atom_bindings.h"
#include "atom/common/api/event_emitter_caller.h"
#include "atom/common/color_util.h"
#include "atom/common/native_mate_converters/value_converter.h"
#include "atom/common/node_bindings.h"
#include "atom/common/node_includes.h"
#include "atom/common/options_switches.h"
#include "atom/renderer/atom_render_view_observer.h"
#include "atom/renderer/guest_view_container.h"
#include "atom/renderer/node_array_buffer_bridge.h"
#include "atom/renderer/preferences_manager.h"
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/renderer/media/chrome_key_systems.h"
@ -29,8 +31,10 @@
#include "content/public/renderer/render_thread.h"
#include "content/public/renderer/render_view.h"
#include "ipc/ipc_message_macros.h"
#include "native_mate/dictionary.h"
#include "net/base/net_errors.h"
#include "third_party/WebKit/public/web/WebCustomElement.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebFrameWidget.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"
#include "third_party/WebKit/public/web/WebPluginParams.h"
@ -59,6 +63,7 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
AtomRenderFrameObserver(content::RenderFrame* frame,
AtomRendererClient* renderer_client)
: content::RenderFrameObserver(frame),
render_frame_(frame),
world_id_(-1),
renderer_client_(renderer_client) {}
@ -69,22 +74,45 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
if (world_id_ != -1 && world_id_ != world_id)
return;
world_id_ = world_id;
renderer_client_->DidCreateScriptContext(context);
renderer_client_->DidCreateScriptContext(context, render_frame_);
}
void WillReleaseScriptContext(v8::Local<v8::Context> context,
int world_id) override {
if (world_id_ != world_id)
return;
renderer_client_->WillReleaseScriptContext(context);
renderer_client_->WillReleaseScriptContext(context, render_frame_);
}
private:
content::RenderFrame* render_frame_;
int world_id_;
AtomRendererClient* renderer_client_;
DISALLOW_COPY_AND_ASSIGN(AtomRenderFrameObserver);
};
v8::Local<v8::Value> GetRenderProcessPreferences(
const PreferencesManager* preferences_manager, v8::Isolate* isolate) {
if (preferences_manager->preferences())
return mate::ConvertToV8(isolate, *preferences_manager->preferences());
else
return v8::Null(isolate);
}
void AddRenderBindings(v8::Isolate* isolate,
v8::Local<v8::Object> process,
const PreferencesManager* preferences_manager) {
mate::Dictionary dict(isolate, process);
dict.SetMethod(
"getRenderProcessPreferences",
base::Bind(GetRenderProcessPreferences, preferences_manager));
}
bool IsDevToolsExtension(content::RenderFrame* render_frame) {
return static_cast<GURL>(render_frame->GetWebFrame()->document().url())
.SchemeIs("chrome-extension");
}
} // namespace
AtomRendererClient::AtomRendererClient()
@ -101,6 +129,8 @@ void AtomRendererClient::RenderThreadStarted() {
OverrideNodeArrayBuffer();
preferences_manager_.reset(new PreferencesManager);
#if defined(OS_WIN)
// Set ApplicationUserModelID in renderer process.
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
@ -128,15 +158,11 @@ void AtomRendererClient::RenderThreadStarted() {
void AtomRendererClient::RenderFrameCreated(
content::RenderFrame* render_frame) {
new PepperHelper(render_frame);
new AtomRenderFrameObserver(render_frame, this);
// Allow file scheme to handle service worker by default.
// FIXME(zcbenz): Can this be moved elsewhere?
blink::WebSecurityPolicy::registerURLSchemeAsAllowingServiceWorkers("file");
// Only insert node integration for the main frame.
if (!render_frame->IsMainFrame())
return;
new AtomRenderFrameObserver(render_frame, this);
}
void AtomRendererClient::RenderViewCreated(content::RenderView* render_view) {
@ -164,6 +190,23 @@ void AtomRendererClient::RunScriptsAtDocumentStart(
// Make sure every page will get a script context created.
render_frame->GetWebFrame()->executeScript(
blink::WebScriptSource("void 0"));
// Inform the document start pharse.
node::Environment* env = node_bindings_->uv_env();
if (env) {
v8::HandleScope handle_scope(env->isolate());
mate::EmitEvent(env->isolate(), env->process_object(), "document-start");
}
}
void AtomRendererClient::RunScriptsAtDocumentEnd(
content::RenderFrame* render_frame) {
// Inform the document end pharse.
node::Environment* env = node_bindings_->uv_env();
if (env) {
v8::HandleScope handle_scope(env->isolate());
mate::EmitEvent(env->isolate(), env->process_object(), "document-end");
}
}
blink::WebSpeechSynthesizer* AtomRendererClient::OverrideSpeechSynthesizer(
@ -186,7 +229,12 @@ bool AtomRendererClient::OverrideCreatePlugin(
}
void AtomRendererClient::DidCreateScriptContext(
v8::Handle<v8::Context> context) {
v8::Handle<v8::Context> context, content::RenderFrame* render_frame) {
// Only allow node integration for the main frame, unless it is a devtools
// extension page.
if (!render_frame->IsMainFrame() && !IsDevToolsExtension(render_frame))
return;
// Whether the node binding has been initialized.
bool first_time = node_bindings_->uv_env() == nullptr;
@ -201,6 +249,8 @@ void AtomRendererClient::DidCreateScriptContext(
// Add atom-shell extended APIs.
atom_bindings_->BindTo(env->isolate(), env->process_object());
AddRenderBindings(env->isolate(), env->process_object(),
preferences_manager_.get());
// Load everything.
node_bindings_->LoadEnvironment(env);
@ -215,9 +265,10 @@ void AtomRendererClient::DidCreateScriptContext(
}
void AtomRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context) {
v8::Handle<v8::Context> context, content::RenderFrame* render_frame) {
node::Environment* env = node::Environment::GetCurrent(context);
mate::EmitEvent(env->isolate(), env->process_object(), "exit");
if (env)
mate::EmitEvent(env->isolate(), env->process_object(), "exit");
}
bool AtomRendererClient::ShouldFork(blink::WebLocalFrame* frame,

View file

@ -13,6 +13,7 @@
namespace atom {
class AtomBindings;
class PreferencesManager;
class NodeBindings;
class AtomRendererClient : public content::ContentRendererClient {
@ -20,8 +21,10 @@ class AtomRendererClient : public content::ContentRendererClient {
AtomRendererClient();
virtual ~AtomRendererClient();
void DidCreateScriptContext(v8::Handle<v8::Context> context);
void WillReleaseScriptContext(v8::Handle<v8::Context> context);
void DidCreateScriptContext(
v8::Handle<v8::Context> context, content::RenderFrame* render_frame);
void WillReleaseScriptContext(
v8::Handle<v8::Context> context, content::RenderFrame* render_frame);
private:
enum NodeIntegration {
@ -36,6 +39,7 @@ class AtomRendererClient : public content::ContentRendererClient {
void RenderFrameCreated(content::RenderFrame*) override;
void RenderViewCreated(content::RenderView*) override;
void RunScriptsAtDocumentStart(content::RenderFrame* render_frame) override;
void RunScriptsAtDocumentEnd(content::RenderFrame* render_frame) override;
blink::WebSpeechSynthesizer* OverrideSpeechSynthesizer(
blink::WebSpeechSynthesizerClient* client) override;
bool OverrideCreatePlugin(content::RenderFrame* render_frame,
@ -61,6 +65,7 @@ class AtomRendererClient : public content::ContentRendererClient {
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<AtomBindings> atom_bindings_;
std::unique_ptr<PreferencesManager> preferences_manager_;
DISALLOW_COPY_AND_ASSIGN(AtomRendererClient);
};

View file

@ -0,0 +1,34 @@
// 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/renderer/preferences_manager.h"
#include "atom/common/api/api_messages.h"
#include "content/public/renderer/render_thread.h"
namespace atom {
PreferencesManager::PreferencesManager() {
content::RenderThread::Get()->AddObserver(this);
}
PreferencesManager::~PreferencesManager() {
}
bool PreferencesManager::OnControlMessageReceived(
const IPC::Message& message) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(PreferencesManager, message)
IPC_MESSAGE_HANDLER(AtomMsg_UpdatePreferences, OnUpdatePreferences)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void PreferencesManager::OnUpdatePreferences(
const base::ListValue& preferences) {
preferences_ = preferences.CreateDeepCopy();
}
} // namespace atom

View file

@ -0,0 +1,35 @@
// 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_RENDERER_PREFERENCES_MANAGER_H_
#define ATOM_RENDERER_PREFERENCES_MANAGER_H_
#include <memory>
#include "base/values.h"
#include "content/public/renderer/render_process_observer.h"
namespace atom {
class PreferencesManager : public content::RenderProcessObserver {
public:
PreferencesManager();
~PreferencesManager() override;
const base::ListValue* preferences() const { return preferences_.get(); }
private:
// content::RenderThreadObserver:
bool OnControlMessageReceived(const IPC::Message& message) override;
void OnUpdatePreferences(const base::ListValue& preferences);
std::unique_ptr<base::ListValue> preferences_;
DISALLOW_COPY_AND_ASSIGN(PreferencesManager);
};
} // namespace atom
#endif // ATOM_RENDERER_PREFERENCES_MANAGER_H_

View file

@ -1,62 +1,52 @@
# DevTools Extension
To make debugging easier, Electron has basic support for the
[Chrome DevTools Extension][devtools-extension].
Electron supports the [Chrome DevTools Extension][devtools-extension], which can
be used to extend the ability of devtools for debugging popular web frameworks.
For most DevTools extensions you can simply download the source code and use
the `BrowserWindow.addDevToolsExtension` API to load them. The loaded extensions
will be remembered so you don't need to call the API every time when creating
a window.
## How to load a DevTools Extension
** NOTE: React DevTools does not work, follow the issue here https://github.com/electron/electron/issues/915 **
To load an extension in Electron, you need to download it in Chrome browser,
locate its filesystem path, and then load it by calling the
`BrowserWindow.addDevToolsExtension(extension)` API.
For example, to use the [React DevTools Extension](https://github.com/facebook/react-devtools)
, first you need to download its source code:
Using the [React Developer Tools][react-devtools] as example:
```bash
$ cd /some-directory
$ git clone --recursive https://github.com/facebook/react-devtools.git
```
1. Install it in Chrome browser.
1. Navigate to `chrome://extensions`, and find its extension ID, which is a hash
string like `fmkadmapgofadopljbjfkapdkoienihi`.
1. Find out filesystem location used by Chrome for storing extensions:
* on Windows it is `%LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions`;
* on Linux it is `~/.config/google-chrome/Default/Extensions/`;
* on OS X it is `~/Library/Application Support/Google/Chrome/Default/Extensions`.
1. Pass the location of the extension to `BrowserWindow.addDevToolsExtension`
API, for the React Developer Tools, it is something like:
`~/Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/0.14.10_0`
Follow the instructions in [`react-devtools/shells/chrome/Readme.md`](https://github.com/facebook/react-devtools/blob/master/shells/chrome/Readme.md) to build the extension.
The name of the extension is returned by `BrowserWindow.addDevToolsExtension`,
and you can pass the name of the extension to the `BrowserWindow.removeDevToolsExtension`
API to unload it.
Then you can load the extension in Electron by opening DevTools in any window,
and running the following code in the DevTools console:
## Supported DevTools Extensions
```javascript
const BrowserWindow = require('electron').remote.BrowserWindow;
BrowserWindow.addDevToolsExtension('/some-directory/react-devtools/shells/chrome');
```
Electron only supports a limited set of `chrome.*` APIs, so some extensions
using unsupported `chrome.*` APIs for chrome extension features may not work.
Following Devtools Extensions are tested and guaranteed to work in Electron:
To unload the extension, you can call the `BrowserWindow.removeDevToolsExtension`
API with its name and it will not load the next time you open the DevTools:
* [Ember Inspector](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
* [React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
* [Backbone Debugger](https://chrome.google.com/webstore/detail/backbone-debugger/bhljhndlimiafopmmhjlgfpnnchjjbhd)
* [jQuery Debugger](https://chrome.google.com/webstore/detail/jquery-debugger/dbhhnnnpaeobfddmlalhnehgclcmjimi)
* [AngularJS Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk)
* [Vue.js devtools](https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
```javascript
BrowserWindow.removeDevToolsExtension('React Developer Tools');
```
### What should I do if a DevTools Extension is not working?
## Format of DevTools Extension
Fist please make sure the extension is still being maintained, some extensions
can not even work for recent versions of Chrome browser, and we are not able to
do anything for them.
Ideally all DevTools extensions written for the Chrome browser can be loaded by
Electron, but they have to be in a plain directory. For those packaged with
`crx` extensions, there is no way for Electron to load them unless you find a
way to extract them into a directory.
## Background Pages
Currently Electron doesn't support the background pages feature in Chrome
extensions, so some DevTools extensions that rely on this feature may
not work in Electron.
## `chrome.*` APIs
Some Chrome extensions may use `chrome.*` APIs for features and while there has
been some effort to implement those APIs in Electron, not all have been
implemented.
Given that not all `chrome.*` APIs are implemented if the DevTools extension is
using APIs other than `chrome.devtools.*`, the extension is very likely not to
work. You can report failing extensions in the issue tracker so that we can add
support for those APIs.
Then file a bug at Electron's issues list, and describe which part of the
extension is not working as expected.
[devtools-extension]: https://developer.chrome.com/extensions/devtools
[react-devtools]: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi

View file

@ -49,6 +49,7 @@
'lib/common/init.js',
'lib/common/reset-search-paths.js',
'lib/renderer/chrome-api.js',
'lib/renderer/content-scripts-injector.js',
'lib/renderer/init.js',
'lib/renderer/inspector.js',
'lib/renderer/override.js',
@ -109,6 +110,8 @@
'atom/browser/api/atom_api_power_monitor.h',
'atom/browser/api/atom_api_power_save_blocker.cc',
'atom/browser/api/atom_api_power_save_blocker.h',
'atom/browser/api/atom_api_render_process_preferences.cc',
'atom/browser/api/atom_api_render_process_preferences.h',
'atom/browser/api/atom_api_protocol.cc',
'atom/browser/api/atom_api_protocol.h',
'atom/browser/api/atom_api_screen.cc',
@ -220,6 +223,8 @@
'atom/browser/net/url_request_fetch_job.h',
'atom/browser/node_debugger.cc',
'atom/browser/node_debugger.h',
'atom/browser/render_process_preferences.cc',
'atom/browser/render_process_preferences.h',
'atom/browser/ui/accelerator_util.cc',
'atom/browser/ui/accelerator_util.h',
'atom/browser/ui/accelerator_util_mac.mm',
@ -396,6 +401,8 @@
'atom/renderer/guest_view_container.h',
'atom/renderer/node_array_buffer_bridge.cc',
'atom/renderer/node_array_buffer_bridge.h',
'atom/renderer/preferences_manager.cc',
'atom/renderer/preferences_manager.h',
'atom/utility/atom_content_utility_client.cc',
'atom/utility/atom_content_utility_client.h',
'chromium_src/chrome/browser/browser_process.cc',

View file

@ -71,12 +71,15 @@ let wrapWebContents = function (webContents) {
webContents.setMaxListeners(0)
// WebContents::send(channel, args..)
webContents.send = function (channel, ...args) {
// WebContents::sendToAll(channel, args..)
const sendWrapper = (allFrames, channel, ...args) => {
if (channel == null) {
throw new Error('Missing required channel argument')
}
return this._send(channel, args)
return webContents._send(allFrames, channel, args)
}
webContents.send = sendWrapper.bind(null, false)
webContents.sendToAll = sendWrapper.bind(null, true)
// The navigation controller.
controller = new NavigationController(webContents)
@ -218,9 +221,12 @@ binding._setWrapWebContents(wrapWebContents)
debuggerBinding._setWrapDebugger(wrapDebugger)
sessionBinding._setWrapSession(wrapSession)
module.exports.create = function (options) {
if (options == null) {
options = {}
module.exports = {
create (options = {}) {
return binding.create(options)
},
fromId (id) {
return binding.fromId(id)
}
return binding.create(options)
}

View file

@ -1,56 +1,224 @@
const {app, protocol, BrowserWindow} = require('electron')
const {app, ipcMain, protocol, webContents, BrowserWindow} = require('electron')
const renderProcessPreferences = process.atomBinding('render_process_preferences').forAllBrowserWindow()
const fs = require('fs')
const path = require('path')
const url = require('url')
// Mapping between hostname and file path.
var hostPathMap = {}
var hostPathMapNextKey = 0
var getHostForPath = function (path) {
var key
key = 'extension-' + (++hostPathMapNextKey)
hostPathMap[key] = path
return key
// TODO(zcbenz): Remove this when we have Object.values().
const objectValues = function (object) {
return Object.keys(object).map(function (key) { return object[key] })
}
var getPathForHost = function (host) {
return hostPathMap[host]
// Mapping between extensionId(hostname) and manifest.
const manifestMap = {} // extensionId => manifest
const manifestNameMap = {} // name => manifest
const generateExtensionIdFromName = function (name) {
return name.replace(/[\W_]+/g, '-').toLowerCase()
}
// Cache extensionInfo.
var extensionInfoMap = {}
var getExtensionInfoFromPath = function (srcDirectory) {
var manifest, page
manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json')))
if (extensionInfoMap[manifest.name] == null) {
// We can not use 'file://' directly because all resources in the extension
// will be treated as relative to the root in Chrome.
page = url.format({
protocol: 'chrome-extension',
slashes: true,
hostname: getHostForPath(srcDirectory),
pathname: manifest.devtools_page
})
extensionInfoMap[manifest.name] = {
startPage: page,
name: manifest.name,
// Create or get manifest object from |srcDirectory|.
const getManifestFromPath = function (srcDirectory) {
const manifest = JSON.parse(fs.readFileSync(path.join(srcDirectory, 'manifest.json')))
if (!manifestNameMap[manifest.name]) {
const extensionId = generateExtensionIdFromName(manifest.name)
console.log(extensionId)
manifestMap[extensionId] = manifestNameMap[manifest.name] = manifest
Object.assign(manifest, {
srcDirectory: srcDirectory,
exposeExperimentalAPIs: true
}
return extensionInfoMap[manifest.name]
extensionId: extensionId,
// We can not use 'file://' directly because all resources in the extension
// will be treated as relative to the root in Chrome.
startPage: url.format({
protocol: 'chrome-extension',
slashes: true,
hostname: extensionId,
pathname: manifest.devtools_page
})
})
return manifest
}
}
// The loaded extensions cache and its persistent path.
var loadedExtensions = null
var loadedExtensionsPath = null
// Manage the background pages.
const backgroundPages = {}
const startBackgroundPages = function (manifest) {
if (backgroundPages[manifest.extensionId] || !manifest.background) return
const scripts = manifest.background.scripts.map((name) => {
return `<script src="${name}"></script>`
}).join('')
const html = new Buffer(`<html><body>${scripts}</body></html>`)
const contents = webContents.create({
commandLineSwitches: ['--background-page']
})
backgroundPages[manifest.extensionId] = { html: html, webContents: contents }
contents.loadURL(url.format({
protocol: 'chrome-extension',
slashes: true,
hostname: manifest.extensionId,
pathname: '_generated_background_page.html'
}))
}
const removeBackgroundPages = function (manifest) {
if (!backgroundPages[manifest.extensionId]) return
backgroundPages[manifest.extensionId].webContents.destroy()
delete backgroundPages[manifest.extensionId]
}
// Dispatch tabs events.
const hookWindowForTabEvents = function (win) {
const tabId = win.webContents.id
for (const page of objectValues(backgroundPages)) {
page.webContents.sendToAll('CHROME_TABS_ONCREATED', tabId)
}
win.once('closed', () => {
for (const page of objectValues(backgroundPages)) {
page.webContents.sendToAll('CHROME_TABS_ONREMOVED', tabId)
}
})
}
// Handle the chrome.* API messages.
let nextId = 0
ipcMain.on('CHROME_RUNTIME_CONNECT', function (event, extensionId, connectInfo) {
const page = backgroundPages[extensionId]
if (!page) {
console.error(`Connect to unkown extension ${extensionId}`)
return
}
const portId = ++nextId
event.returnValue = {tabId: page.webContents.id, portId: portId}
event.sender.once('render-view-deleted', () => {
if (page.webContents.isDestroyed()) return
page.webContents.sendToAll(`CHROME_PORT_DISCONNECT_${portId}`)
})
page.webContents.sendToAll(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, event.sender.id, portId, connectInfo)
})
ipcMain.on('CHROME_RUNTIME_SENDMESSAGE', function (event, extensionId, message) {
const page = backgroundPages[extensionId]
if (!page) {
console.error(`Connect to unkown extension ${extensionId}`)
return
}
page.webContents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, event.sender.id, message)
})
ipcMain.on('CHROME_TABS_SEND_MESSAGE', function (event, tabId, extensionId, isBackgroundPage, message) {
const contents = webContents.fromId(tabId)
if (!contents) {
console.error(`Sending message to unkown tab ${tabId}`)
return
}
const senderTabId = isBackgroundPage ? null : event.sender.id
contents.sendToAll(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, senderTabId, message)
})
ipcMain.on('CHROME_TABS_EXECUTESCRIPT', function (event, requestId, tabId, extensionId, details) {
const contents = webContents.fromId(tabId)
if (!contents) {
console.error(`Sending message to unkown tab ${tabId}`)
return
}
let code, url
if (details.file) {
const manifest = manifestMap[extensionId]
code = String(fs.readFileSync(path.join(manifest.srcDirectory, details.file)))
url = `chrome-extension://${extensionId}${details.file}`
} else {
code = details.code
url = `chrome-extension://${extensionId}/${String(Math.random()).substr(2, 8)}.js`
}
contents.send('CHROME_TABS_EXECUTESCRIPT', event.sender.id, requestId, extensionId, url, code)
})
// Transfer the content scripts to renderer.
const contentScripts = {}
const injectContentScripts = function (manifest) {
if (contentScripts[manifest.name] || !manifest.content_scripts) return
const readArrayOfFiles = function (relativePath) {
return {
url: `chrome-extension://${manifest.extensionId}/${relativePath}`,
code: String(fs.readFileSync(path.join(manifest.srcDirectory, relativePath)))
}
}
const contentScriptToEntry = function (script) {
return {
matches: script.matches,
js: script.js.map(readArrayOfFiles),
runAt: script.run_at || 'document_idle'
}
}
try {
const entry = {
extensionId: manifest.extensionId,
contentScripts: manifest.content_scripts.map(contentScriptToEntry)
}
contentScripts[manifest.name] = renderProcessPreferences.addEntry(entry)
} catch (e) {
console.error('Failed to read content scripts', e)
}
}
const removeContentScripts = function (manifest) {
if (!contentScripts[manifest.name]) return
renderProcessPreferences.removeEntry(contentScripts[manifest.name])
delete contentScripts[manifest.name]
}
// Transfer the |manifest| to a format that can be recognized by the
// |DevToolsAPI.addExtensions|.
const manifestToExtensionInfo = function (manifest) {
return {
startPage: manifest.startPage,
srcDirectory: manifest.srcDirectory,
name: manifest.name,
exposeExperimentalAPIs: true
}
}
// Load the extensions for the window.
const loadExtension = function (manifest) {
startBackgroundPages(manifest)
injectContentScripts(manifest)
}
const loadDevToolsExtensions = function (win, manifests) {
if (!win.devToolsWebContents) return
manifests.forEach(loadExtension)
const extensionInfoArray = manifests.map(manifestToExtensionInfo)
win.devToolsWebContents.executeJavaScript(`DevToolsAPI.addExtensions(${JSON.stringify(extensionInfoArray)})`)
}
// The persistent path of "DevTools Extensions" preference file.
let loadedExtensionsPath = null
app.on('will-quit', function () {
try {
loadedExtensions = Object.keys(extensionInfoMap).map(function (key) {
return extensionInfoMap[key].srcDirectory
const loadedExtensions = objectValues(manifestMap).map(function (manifest) {
return manifest.srcDirectory
})
if (loadedExtensions.length > 0) {
try {
@ -69,74 +237,78 @@ app.on('will-quit', function () {
// We can not use protocol or BrowserWindow until app is ready.
app.once('ready', function () {
var chromeExtensionHandler, i, init, len, srcDirectory
// The chrome-extension: can map a extension URL request to real file path.
const chromeExtensionHandler = function (request, callback) {
const parsed = url.parse(request.url)
if (!parsed.hostname || !parsed.path) return callback()
const manifest = manifestMap[parsed.hostname]
if (!manifest) return callback()
if (parsed.path === '/_generated_background_page.html' &&
backgroundPages[parsed.hostname]) {
return callback({
mimeType: 'text/html',
data: backgroundPages[parsed.hostname].html
})
}
fs.readFile(path.join(manifest.srcDirectory, parsed.path), function (err, content) {
if (err) {
return callback(-6) // FILE_NOT_FOUND
} else {
return callback(content)
}
})
}
protocol.registerBufferProtocol('chrome-extension', chromeExtensionHandler, function (error) {
if (error) {
console.error(`Unable to register chrome-extension protocol: ${error}`)
}
})
// Load persisted extensions.
loadedExtensionsPath = path.join(app.getPath('userData'), 'DevTools Extensions')
try {
loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath))
if (!Array.isArray(loadedExtensions)) {
loadedExtensions = []
}
// Preheat the extensionInfo cache.
for (i = 0, len = loadedExtensions.length; i < len; i++) {
srcDirectory = loadedExtensions[i]
getExtensionInfoFromPath(srcDirectory)
const loadedExtensions = JSON.parse(fs.readFileSync(loadedExtensionsPath))
if (Array.isArray(loadedExtensions)) {
for (const srcDirectory of loadedExtensions) {
// Start background pages and set content scripts.
const manifest = getManifestFromPath(srcDirectory)
loadExtension(manifest)
}
}
} catch (error) {
// Ignore error
}
// The chrome-extension: can map a extension URL request to real file path.
chromeExtensionHandler = function (request, callback) {
var directory, parsed
parsed = url.parse(request.url)
if (!(parsed.hostname && (parsed.path != null))) {
return callback()
}
if (!/extension-\d+/.test(parsed.hostname)) {
return callback()
}
directory = getPathForHost(parsed.hostname)
if (directory == null) {
return callback()
}
return callback(path.join(directory, parsed.path))
}
protocol.registerFileProtocol('chrome-extension', chromeExtensionHandler, function (error) {
if (error) {
return console.error('Unable to register chrome-extension protocol')
}
})
BrowserWindow.prototype._loadDevToolsExtensions = function (extensionInfoArray) {
var ref
return (ref = this.devToolsWebContents) != null ? ref.executeJavaScript('DevToolsAPI.addExtensions(' + (JSON.stringify(extensionInfoArray)) + ');') : void 0
}
// The public API to add/remove extensions.
BrowserWindow.addDevToolsExtension = function (srcDirectory) {
var extensionInfo, j, len1, ref, window
extensionInfo = getExtensionInfoFromPath(srcDirectory)
if (extensionInfo) {
ref = BrowserWindow.getAllWindows()
for (j = 0, len1 = ref.length; j < len1; j++) {
window = ref[j]
window._loadDevToolsExtensions([extensionInfo])
const manifest = getManifestFromPath(srcDirectory)
if (manifest) {
for (const win of BrowserWindow.getAllWindows()) {
loadDevToolsExtensions(win, [manifest])
}
return extensionInfo.name
return manifest.name
}
}
BrowserWindow.removeDevToolsExtension = function (name) {
return delete extensionInfoMap[name]
const manifest = manifestNameMap[name]
if (!manifest) return
removeBackgroundPages(manifest)
removeContentScripts(manifest)
delete manifestMap[manifest.extensionId]
delete manifestNameMap[name]
}
// Load persisted extensions when devtools is opened.
init = BrowserWindow.prototype._init
// Load extensions automatically when devtools is opened.
const init = BrowserWindow.prototype._init
BrowserWindow.prototype._init = function () {
init.call(this)
return this.webContents.on('devtools-opened', () => {
return this._loadDevToolsExtensions(Object.keys(extensionInfoMap).map(function (key) {
return extensionInfoMap[key]
}))
hookWindowForTabEvents(this)
this.webContents.on('devtools-opened', () => {
loadDevToolsExtensions(this, objectValues(manifestMap))
})
}
})

View file

@ -2,7 +2,7 @@
const electron = require('electron')
const v8Util = process.atomBinding('v8_util')
const {ipcMain, isPromise} = electron
const {ipcMain, isPromise, webContents} = electron
const objectsRegistry = require('./objects-registry')
@ -353,3 +353,17 @@ ipcMain.on('ELECTRON_BROWSER_ASYNC_CALL_TO_GUEST_VIEW', function (event, request
event.returnValue = exceptionToMeta(error)
}
})
ipcMain.on('ELECTRON_BROWSER_SEND_TO', function (event, sendToAll, webContentsId, channel, ...args) {
let contents = webContents.fromId(webContentsId)
if (!contents) {
console.error(`Sending message to WebContents with unknown ID ${webContentsId}`)
return
}
if (sendToAll) {
contents.sendToAll(channel, ...args)
} else {
contents.send(channel, ...args)
}
})

View file

@ -18,4 +18,20 @@ ipcRenderer.sendToHost = function (...args) {
return binding.send('ipc-message-host', args)
}
ipcRenderer.sendTo = function (webContentsId, channel, ...args) {
if (typeof webContentsId !== 'number') {
throw new TypeError('First argument has to be webContentsId')
}
ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', false, webContentsId, channel, ...args)
}
ipcRenderer.sendToAll = function (webContentsId, channel, ...args) {
if (typeof webContentsId !== 'number') {
throw new TypeError('First argument has to be webContentsId')
}
ipcRenderer.send('ELECTRON_BROWSER_SEND_TO', true, webContentsId, channel, ...args)
}
module.exports = ipcRenderer

View file

@ -1,13 +1,200 @@
const {ipcRenderer} = require('electron')
const url = require('url')
const chrome = window.chrome = window.chrome || {}
chrome.extension = {
getURL: function (path) {
return url.format({
protocol: window.location.protocol,
slashes: true,
hostname: window.location.hostname,
pathname: path
})
let nextId = 0
class Event {
constructor () {
this.listeners = []
}
addListener (callback) {
this.listeners.push(callback)
}
removeListener (callback) {
const index = this.listeners.indexOf(callback)
if (index !== -1) {
this.listeners.splice(index, 1)
}
}
emit (...args) {
for (const listener of this.listeners) {
listener(...args)
}
}
}
class Tab {
constructor (tabId) {
this.id = tabId
}
}
class MessageSender {
constructor (tabId, extensionId) {
this.tab = tabId ? new Tab(tabId) : null
this.id = extensionId
this.url = `chrome-extension://${extensionId}`
}
}
class Port {
constructor (tabId, portId, extensionId, name) {
this.tabId = tabId
this.portId = portId
this.disconnected = false
this.name = name
this.onDisconnect = new Event()
this.onMessage = new Event()
this.sender = new MessageSender(tabId, extensionId)
ipcRenderer.once(`CHROME_PORT_DISCONNECT_${portId}`, () => {
this._onDisconnect()
})
ipcRenderer.on(`CHROME_PORT_POSTMESSAGE_${portId}`, (event, message) => {
const sendResponse = function () { console.error('sendResponse is not implemented') }
this.onMessage.emit(message, this.sender, sendResponse)
})
}
disconnect () {
if (this.disconnected) return
ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_DISCONNECT_${this.portId}`)
this._onDisconnect()
}
postMessage (message) {
ipcRenderer.sendToAll(this.tabId, `CHROME_PORT_POSTMESSAGE_${this.portId}`, message)
}
_onDisconnect () {
this.disconnected = true
ipcRenderer.removeAllListeners(`CHROME_PORT_POSTMESSAGE_${this.portId}`)
this.onDisconnect.emit()
}
}
// Inject chrome API to the |context|
exports.injectTo = function (extensionId, isBackgroundPage, context) {
const chrome = context.chrome = context.chrome || {}
ipcRenderer.on(`CHROME_RUNTIME_ONCONNECT_${extensionId}`, (event, tabId, portId, connectInfo) => {
chrome.runtime.onConnect.emit(new Port(tabId, portId, extensionId, connectInfo.name))
})
ipcRenderer.on(`CHROME_RUNTIME_ONMESSAGE_${extensionId}`, (event, tabId, message) => {
chrome.runtime.onMessage.emit(message, new MessageSender(tabId, extensionId))
})
ipcRenderer.on('CHROME_TABS_ONCREATED', (event, tabId) => {
chrome.tabs.onCreated.emit(new Tab(tabId))
})
ipcRenderer.on('CHROME_TABS_ONREMOVED', (event, tabId) => {
chrome.tabs.onRemoved.emit(tabId)
})
chrome.runtime = {
getURL: function (path) {
return url.format({
protocol: 'chrome-extension',
slashes: true,
hostname: extensionId,
pathname: path
})
},
connect (...args) {
if (isBackgroundPage) {
console.error('chrome.runtime.connect is not supported in background page')
return
}
// Parse the optional args.
let targetExtensionId = extensionId
let connectInfo = {name: ''}
if (args.length === 1) {
connectInfo = args[0]
} else if (args.length === 2) {
[targetExtensionId, connectInfo] = args
}
const {tabId, portId} = ipcRenderer.sendSync('CHROME_RUNTIME_CONNECT', targetExtensionId, connectInfo)
return new Port(tabId, portId, extensionId, connectInfo.name)
},
sendMessage (...args) {
if (isBackgroundPage) {
console.error('chrome.runtime.sendMessage is not supported in background page')
return
}
// Parse the optional args.
let targetExtensionId = extensionId
let message
if (args.length === 1) {
message = args[0]
} else if (args.length === 2) {
[targetExtensionId, message] = args
} else {
console.error('options and responseCallback are not supported')
}
ipcRenderer.send('CHROME_RUNTIME_SENDMESSAGE', targetExtensionId, message)
},
onConnect: new Event(),
onMessage: new Event(),
onInstalled: new Event()
}
chrome.tabs = {
executeScript (tabId, details, callback) {
const requestId = ++nextId
ipcRenderer.once(`CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, (event, result) => {
callback([event.result])
})
ipcRenderer.send('CHROME_TABS_EXECUTESCRIPT', requestId, tabId, extensionId, details)
},
sendMessage (tabId, message, options, responseCallback) {
if (responseCallback) {
console.error('responseCallback is not supported')
}
ipcRenderer.send('CHROME_TABS_SEND_MESSAGE', tabId, extensionId, isBackgroundPage, message)
},
onUpdated: new Event(),
onCreated: new Event(),
onRemoved: new Event()
}
chrome.extension = {
getURL: chrome.runtime.getURL,
connect: chrome.runtime.connect,
onConnect: chrome.runtime.onConnect,
sendMessage: chrome.runtime.sendMessage,
onMessage: chrome.runtime.onMessage
}
chrome.storage = {
sync: {
get () {},
set () {}
}
}
chrome.pageAction = {
show () {},
hide () {},
setTitle () {},
getTitle () {},
setIcon () {},
setPopup () {},
getPopup () {}
}
}

View file

@ -0,0 +1,61 @@
const {ipcRenderer} = require('electron')
const {runInThisContext} = require('vm')
// Check whether pattern matches.
// https://developer.chrome.com/extensions/match_patterns
const matchesPattern = function (pattern) {
if (pattern === '<all_urls>') return true
const regexp = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$')
return location.href.match(regexp)
}
// Run the code with chrome API integrated.
const runContentScript = function (extensionId, url, code) {
const context = {}
require('./chrome-api').injectTo(extensionId, false, context)
const wrapper = `(function (chrome) {\n ${code}\n })`
const compiledWrapper = runInThisContext(wrapper, {
filename: url,
lineOffset: 1,
displayErrors: true
})
return compiledWrapper.call(this, context.chrome)
}
// Run injected scripts.
// https://developer.chrome.com/extensions/content_scripts
const injectContentScript = function (extensionId, script) {
for (const match of script.matches) {
if (!matchesPattern(match)) return
}
for (const {url, code} of script.js) {
const fire = runContentScript.bind(window, extensionId, url, code)
if (script.runAt === 'document_start') {
process.once('document-start', fire)
} else if (script.runAt === 'document_end') {
process.once('document-end', fire)
} else if (script.runAt === 'document_idle') {
document.addEventListener('DOMContentLoaded', fire)
}
}
}
// Handle the request of chrome.tabs.executeJavaScript.
ipcRenderer.on('CHROME_TABS_EXECUTESCRIPT', function (event, senderWebContentsId, requestId, extensionId, url, code) {
const result = runContentScript.call(window, extensionId, url, code)
ipcRenderer.sendToAll(senderWebContentsId, `CHROME_TABS_EXECUTESCRIPT_RESULT_${requestId}`, result)
})
// Read the renderer process preferences.
const preferences = process.getRenderProcessPreferences()
if (preferences) {
for (const pref of preferences) {
if (pref.contentScripts) {
for (const script of pref.contentScripts) {
injectContentScript(pref.extensionId, script)
}
}
}
}

View file

@ -41,8 +41,9 @@ electron.ipcRenderer.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (ev
})
// Process command line arguments.
var nodeIntegration = 'false'
var preloadScript = null
let nodeIntegration = 'false'
let preloadScript = null
let isBackgroundPage = false
for (let arg of process.argv) {
if (arg.indexOf('--guest-instance-id=') === 0) {
// This is a guest web view.
@ -54,6 +55,8 @@ for (let arg of process.argv) {
nodeIntegration = arg.substr(arg.indexOf('=') + 1)
} else if (arg.indexOf('--preload=') === 0) {
preloadScript = arg.substr(arg.indexOf('=') + 1)
} else if (arg === '--background-page') {
isBackgroundPage = true
}
}
@ -63,12 +66,15 @@ if (window.location.protocol === 'chrome-devtools:') {
nodeIntegration = 'true'
} else if (window.location.protocol === 'chrome-extension:') {
// Add implementations of chrome API.
require('./chrome-api')
nodeIntegration = 'true'
require('./chrome-api').injectTo(window.location.hostname, isBackgroundPage, window)
nodeIntegration = 'false'
} else {
// Override default web functions.
require('./override')
// Inject content scripts.
require('./content-scripts-injector')
// Load webview tag implementation.
if (nodeIntegration === 'true' && process.guestInstanceId == null) {
require('./web-view/web-view')

View file

@ -1,7 +1,4 @@
window.onload = function () {
// Make sure |window.chrome| is defined for devtools extensions.
hijackSetInjectedScript(window.InspectorFrontendHost)
// Use menu API to show context menu.
window.InspectorFrontendHost.showContextMenuAtPoint = createMenu
@ -9,18 +6,6 @@ window.onload = function () {
window.WebInspector.createFileSelectorElement = createFileSelectorElement
}
const hijackSetInjectedScript = function (InspectorFrontendHost) {
const {setInjectedScriptForOrigin} = InspectorFrontendHost
InspectorFrontendHost.setInjectedScriptForOrigin = function (origin, source) {
const wrapped = `(function (...args) {
window.chrome = {}
const original = ${source}
original(...args)
})`
setInjectedScriptForOrigin(origin, wrapped)
}
}
var convertToMenuTemplate = function (items) {
var fn, i, item, len, template
template = []

View file

@ -3,11 +3,8 @@
const assert = require('assert')
const path = require('path')
const ipcRenderer = require('electron').ipcRenderer
const remote = require('electron').remote
const ipcMain = remote.require('electron').ipcMain
const BrowserWindow = remote.require('electron').BrowserWindow
const {ipcRenderer, remote} = require('electron')
const {ipcMain, webContents, BrowserWindow} = remote
const comparePaths = function (path1, path2) {
if (process.platform === 'win32') {
@ -244,6 +241,30 @@ describe('ipc module', function () {
})
})
describe('ipcRenderer.sendTo', function () {
let contents = null
beforeEach(function () {
contents = webContents.create({})
})
afterEach(function () {
ipcRenderer.removeAllListeners('pong')
contents.destroy()
contents = null
})
it('sends message to WebContents', function (done) {
const webContentsId = remote.getCurrentWebContents().id
ipcRenderer.once('pong', function (event, id) {
assert.equal(webContentsId, id)
done()
})
contents.once('did-finish-load', function () {
ipcRenderer.sendTo(contents.id, 'ping', webContentsId)
})
contents.loadURL('file://' + path.join(fixtures, 'pages', 'ping-pong.html'))
})
})
describe('remote listeners', function () {
var w = null

11
spec/fixtures/pages/ping-pong.html vendored Normal file
View file

@ -0,0 +1,11 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
const {ipcRenderer} = require('electron')
ipcRenderer.on('ping', function (event, id) {
ipcRenderer.sendTo(id, 'pong', id)
})
</script>
</body>
</html>