Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature : Affinity option for gathering several pages in a single process #11501

Merged
merged 3 commits into from Feb 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
86 changes: 65 additions & 21 deletions atom/browser/atom_browser_client.cc
Expand Up @@ -167,11 +167,14 @@ void AtomBrowserClient::RenderProcessWillLaunch(

content::WebContents* web_contents = GetWebContentsFromProcessID(process_id);
ProcessPreferences process_prefs;
process_prefs.sandbox = WebContentsPreferences::IsSandboxed(web_contents);
process_prefs.native_window_open
= WebContentsPreferences::UsesNativeWindowOpen(web_contents);
process_prefs.disable_popups
= WebContentsPreferences::DisablePopups(web_contents);
process_prefs.sandbox =
WebContentsPreferences::IsPreferenceEnabled("sandbox", web_contents);
process_prefs.native_window_open =
WebContentsPreferences::IsPreferenceEnabled("nativeWindowOpen",
web_contents);
process_prefs.disable_popups =
WebContentsPreferences::IsPreferenceEnabled("disablePopups",
web_contents);
AddProcessPreferences(host->GetID(), process_prefs);
// ensure the ProcessPreferences is removed later
host->AddObserver(this);
Expand Down Expand Up @@ -204,7 +207,7 @@ void AtomBrowserClient::OverrideWebkitPrefs(
}

void AtomBrowserClient::OverrideSiteInstanceForNavigation(
content::RenderFrameHost* render_frame_host,
content::RenderFrameHost* rfh,
content::BrowserContext* browser_context,
content::SiteInstance* current_instance,
const GURL& url,
Expand All @@ -214,25 +217,53 @@ void AtomBrowserClient::OverrideSiteInstanceForNavigation(
return;
}

if (!ShouldCreateNewSiteInstance(render_frame_host, browser_context,
current_instance, url))
if (!ShouldCreateNewSiteInstance(rfh, browser_context, current_instance, url))
return;

scoped_refptr<content::SiteInstance> site_instance =
content::SiteInstance::CreateForURL(browser_context, url);
bool is_new_instance = true;
scoped_refptr<content::SiteInstance> site_instance;

// Do we have an affinity site to manage ?
std::string affinity;
auto* web_contents = content::WebContents::FromRenderFrameHost(rfh);
auto* web_preferences = web_contents ?
WebContentsPreferences::FromWebContents(web_contents) : nullptr;
if (web_preferences &&
web_preferences->web_preferences()->GetString("affinity", &affinity) &&
!affinity.empty()) {
affinity = base::ToLowerASCII(affinity);
auto iter = site_per_affinities.find(affinity);
if (iter != site_per_affinities.end()) {
site_instance = iter->second;
is_new_instance = false;
} else {
// We must not provide the url.
// This site is "isolated" and must not be taken into account
// when Chromium looking at a candidate for an url.
site_instance = content::SiteInstance::Create(
browser_context);
site_per_affinities[affinity] = site_instance.get();
}
} else {
site_instance = content::SiteInstance::CreateForURL(
browser_context,
url);
}
*new_instance = site_instance.get();

// Make sure the |site_instance| is not freed when this function returns.
// FIXME(zcbenz): We should adjust OverrideSiteInstanceForNavigation's
// interface to solve this.
content::BrowserThread::PostTask(
content::BrowserThread::UI, FROM_HERE,
base::Bind(&Noop, base::RetainedRef(site_instance)));

// Remember the original web contents for the pending renderer process.
auto pending_process = (*new_instance)->GetProcess();
pending_processes_[pending_process->GetID()] =
content::WebContents::FromRenderFrameHost(render_frame_host);
if (is_new_instance) {
// Make sure the |site_instance| is not freed
// when this function returns.
// FIXME(zcbenz): We should adjust
// OverrideSiteInstanceForNavigation's interface to solve this.
content::BrowserThread::PostTask(
content::BrowserThread::UI, FROM_HERE,
base::Bind(&Noop, base::RetainedRef(site_instance)));

// Remember the original web contents for the pending renderer process.
auto pending_process = site_instance->GetProcess();
pending_processes_[pending_process->GetID()] = web_contents;
}
}

void AtomBrowserClient::AppendExtraCommandLineSwitches(
Expand Down Expand Up @@ -396,6 +427,19 @@ void AtomBrowserClient::GetAdditionalAllowedSchemesForFileSystem(
additional_schemes->push_back(content::kChromeDevToolsScheme);
}

void AtomBrowserClient::SiteInstanceDeleting(
content::SiteInstance* site_instance) {
// We are storing weak_ptr, is it fundamental to maintain the map up-to-date
// when an instance is destroyed.
for (auto iter = site_per_affinities.begin();
iter != site_per_affinities.end(); ++iter) {
if (iter->second == site_instance) {
site_per_affinities.erase(iter);
break;
}
}
}

brightray::BrowserMainParts* AtomBrowserClient::OverrideCreateBrowserMainParts(
const content::MainFunctionParams&) {
v8::V8::Initialize(); // Init V8 before creating main parts.
Expand Down
11 changes: 8 additions & 3 deletions atom/browser/atom_browser_client.h
Expand Up @@ -98,6 +98,7 @@ class AtomBrowserClient : public brightray::BrowserClient,
bool* no_javascript_access) override;
void GetAdditionalAllowedSchemesForFileSystem(
std::vector<std::string>* schemes) override;
void SiteInstanceDeleting(content::SiteInstance* site_instance) override;

// brightray::BrowserClient:
brightray::BrowserMainParts* OverrideCreateBrowserMainParts(
Expand All @@ -119,9 +120,9 @@ class AtomBrowserClient : public brightray::BrowserClient,
content::SiteInstance* current_instance,
const GURL& dest_url);
struct ProcessPreferences {
bool sandbox;
bool native_window_open;
bool disable_popups;
bool sandbox = false;
bool native_window_open = false;
bool disable_popups = false;
};
void AddProcessPreferences(int process_id, ProcessPreferences prefs);
void RemoveProcessPreferences(int process_id);
Expand All @@ -134,6 +135,10 @@ class AtomBrowserClient : public brightray::BrowserClient,

std::map<int, ProcessPreferences> process_preferences_;
std::map<int, base::ProcessId> render_process_host_pids_;

// list of site per affinity. weak_ptr to prevent instance locking
std::map<std::string, content::SiteInstance*> site_per_affinities;

base::Lock process_preferences_lock_;

std::unique_ptr<AtomResourceDispatcherHostDelegate>
Expand Down
2 changes: 1 addition & 1 deletion atom/browser/atom_resource_dispatcher_host_delegate.cc
Expand Up @@ -75,7 +75,7 @@ void OnPdfResourceIntercepted(
if (!web_contents)
return;

if (!WebContentsPreferences::IsPluginsEnabled(web_contents)) {
if (!WebContentsPreferences::IsPreferenceEnabled("plugins", web_contents)) {
auto browser_context = web_contents->GetBrowserContext();
auto download_manager =
content::BrowserContext::GetDownloadManager(browser_context);
Expand Down
22 changes: 2 additions & 20 deletions atom/browser/web_contents_preferences.cc
Expand Up @@ -112,7 +112,8 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
// If the `sandbox` option was passed to the BrowserWindow's webPreferences,
// pass `--enable-sandbox` to the renderer so it won't have any node.js
// integration.
if (IsSandboxed(web_contents)) {
bool sandbox = false;
if (web_preferences.GetBoolean("sandbox", &sandbox) && sandbox) {
command_line->AppendSwitch(switches::kEnableSandbox);
} else if (!command_line->HasSwitch(switches::kEnableSandbox)) {
command_line->AppendSwitch(::switches::kNoSandbox);
Expand Down Expand Up @@ -237,25 +238,6 @@ bool WebContentsPreferences::IsPreferenceEnabled(
return bool_value;
}

bool WebContentsPreferences::IsSandboxed(content::WebContents* web_contents) {
return IsPreferenceEnabled("sandbox", web_contents);
}

bool WebContentsPreferences::UsesNativeWindowOpen(
content::WebContents* web_contents) {
return IsPreferenceEnabled("nativeWindowOpen", web_contents);
}

bool WebContentsPreferences::IsPluginsEnabled(
content::WebContents* web_contents) {
return IsPreferenceEnabled("plugins", web_contents);
}

bool WebContentsPreferences::DisablePopups(
content::WebContents* web_contents) {
return IsPreferenceEnabled("disablePopups", web_contents);
}

// static
void WebContentsPreferences::OverrideWebkitPrefs(
content::WebContents* web_contents, content::WebPreferences* prefs) {
Expand Down
4 changes: 0 additions & 4 deletions atom/browser/web_contents_preferences.h
Expand Up @@ -39,10 +39,6 @@ class WebContentsPreferences

static bool IsPreferenceEnabled(const std::string& attribute_name,
content::WebContents* web_contents);
static bool IsSandboxed(content::WebContents* web_contents);
static bool UsesNativeWindowOpen(content::WebContents* web_contents);
static bool DisablePopups(content::WebContents* web_contents);
static bool IsPluginsEnabled(content::WebContents* web_contents);

// Modify the WebPreferences according to |web_contents|'s preferences.
static void OverrideWebkitPrefs(
Expand Down
7 changes: 7 additions & 0 deletions docs/api/browser-window.md
Expand Up @@ -280,6 +280,13 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
same `partition`. If there is no `persist:` prefix, the page will use an
in-memory session. By assigning the same `partition`, multiple pages can share
the same session. Default is the default session.
* `affinity` String (optional) - When specified, web pages with the same
`affinity` will run in the same renderer process. Note that due to reusing
the renderer process, certain `webPreferences` options will also be shared
between the web pages even when you specified different values for them,
including but not limited to `preload`, `sandbox` and `nodeIntegration`.
So it is suggested to use exact same `webPreferences` for web pages with
the same `affinity`.
* `zoomFactor` Number (optional) - The default zoom factor of the page, `3.0` represents
`300%`. Default is `1.0`.
* `javascript` Boolean (optional) - Enables JavaScript support. Default is `true`.
Expand Down
153 changes: 153 additions & 0 deletions spec/api-browser-window-affinity-spec.js
@@ -0,0 +1,153 @@
'use strict'

const assert = require('assert')
const path = require('path')

const { remote } = require('electron')
const { ipcMain, BrowserWindow } = remote
const {closeWindow} = require('./window-helpers')

describe('BrowserWindow with affinity module', () => {
const fixtures = path.resolve(__dirname, 'fixtures')
const myAffinityName = 'myAffinity'
const myAffinityNameUpper = 'MYAFFINITY'
const anotherAffinityName = 'anotherAffinity'

function createWindowWithWebPrefs (webPrefs) {
return new Promise((resolve, reject) => {
const w = new BrowserWindow({
show: false,
width: 400,
height: 400,
webPreferences: webPrefs || {}
})
w.webContents.on('did-finish-load', () => {
resolve(w)
})
w.loadURL('file://' + path.join(fixtures, 'api', 'blank.html'))
})
}

describe(`BrowserWindow with an affinity '${myAffinityName}'`, () => {
let mAffinityWindow
before((done) => {
createWindowWithWebPrefs({ affinity: myAffinityName })
.then((w) => {
mAffinityWindow = w
done()
})
})

after((done) => {
closeWindow(mAffinityWindow, {assertSingleWindow: false}).then(() => {
mAffinityWindow = null
done()
})
})

it('should have a different process id than a default window', (done) => {
createWindowWithWebPrefs({})
.then((w) => {
assert.notEqual(mAffinityWindow.webContents.getOSProcessId(), w.webContents.getOSProcessId(), 'Should have the different OS process Id/s')
closeWindow(w, {assertSingleWindow: false}).then(() => {
done()
})
})
})

it(`should have a different process id than a window with a different affinity '${anotherAffinityName}'`, (done) => {
createWindowWithWebPrefs({ affinity: anotherAffinityName })
.then((w) => {
assert.notEqual(mAffinityWindow.webContents.getOSProcessId(), w.webContents.getOSProcessId(), 'Should have the different OS process Id/s')
closeWindow(w, {assertSingleWindow: false}).then(() => {
done()
})
})
})

it(`should have the same OS process id than a window with the same affinity '${myAffinityName}'`, (done) => {
createWindowWithWebPrefs({ affinity: myAffinityName })
.then((w) => {
assert.equal(mAffinityWindow.webContents.getOSProcessId(), w.webContents.getOSProcessId(), 'Should have the same OS process Id')
closeWindow(w, {assertSingleWindow: false}).then(() => {
done()
})
})
})

it(`should have the same OS process id than a window with an equivalent affinity '${myAffinityNameUpper}' (case insensitive)`, (done) => {
createWindowWithWebPrefs({ affinity: myAffinityNameUpper })
.then((w) => {
assert.equal(mAffinityWindow.webContents.getOSProcessId(), w.webContents.getOSProcessId(), 'Should have the same OS process Id')
closeWindow(w, {assertSingleWindow: false}).then(() => {
done()
})
})
})
})

describe(`BrowserWindow with an affinity : nodeIntegration=false`, () => {
const preload = path.join(fixtures, 'module', 'send-later.js')
const affinityWithNodeTrue = 'affinityWithNodeTrue'
const affinityWithNodeFalse = 'affinityWithNodeFalse'

function testNodeIntegration (present) {
return new Promise((resolve, reject) => {
ipcMain.once('answer', (event, typeofProcess, typeofBuffer) => {
if (present) {
assert.notEqual(typeofProcess, 'undefined')
assert.notEqual(typeofBuffer, 'undefined')
} else {
assert.equal(typeofProcess, 'undefined')
assert.equal(typeofBuffer, 'undefined')
}
resolve()
})
})
}

it('disables node integration when specified to false', (done) => {
Promise.all([testNodeIntegration(false), createWindowWithWebPrefs({ affinity: affinityWithNodeTrue, preload: preload, nodeIntegration: false })])
.then((args) => {
closeWindow(args[1], {assertSingleWindow: false}).then(() => {
done()
})
})
})
it('disables node integration when first window is false', (done) => {
Promise.all([testNodeIntegration(false), createWindowWithWebPrefs({ affinity: affinityWithNodeTrue, preload: preload, nodeIntegration: false })])
.then((args) => {
let w1 = args[1]
return Promise.all([testNodeIntegration(false), w1, createWindowWithWebPrefs({ affinity: affinityWithNodeTrue, preload: preload, nodeIntegration: true })])
})
.then((ws) => {
return Promise.all([closeWindow(ws[1], {assertSingleWindow: false}), closeWindow(ws[2], {assertSingleWindow: false})])
})
.then(() => {
done()
})
})

it('enables node integration when specified to true', (done) => {
Promise.all([testNodeIntegration(true), createWindowWithWebPrefs({ affinity: affinityWithNodeFalse, preload: preload, nodeIntegration: true })])
.then((args) => {
closeWindow(args[1], {assertSingleWindow: false}).then(() => {
done()
})
})
})
it('enables node integration when first window is true', (done) => {
Promise.all([testNodeIntegration(true), createWindowWithWebPrefs({ affinity: affinityWithNodeFalse, preload: preload, nodeIntegration: true })])
.then((args) => {
let w1 = args[1]
return Promise.all([testNodeIntegration(true), w1, createWindowWithWebPrefs({ affinity: affinityWithNodeFalse, preload: preload, nodeIntegration: false })])
})
.then((ws) => {
return Promise.all([closeWindow(ws[1], {assertSingleWindow: false}), closeWindow(ws[2], {assertSingleWindow: false})])
})
.then(() => {
done()
})
})
})
})