feat(Browser): make browser.close() to always terminate remote browser

This patch:
- changes `browser.close` to terminate browser.
- introduces new `browser.disconnect` to disconnect from a browser without closing it

This patch: fixes #918, fixes #989 

BREAKING CHANGE:
`browser.close()` will always close a browser, even if it was initialized with
`puppeteer.connect`. To disconnect from a remote browser, use `browser.disconnect()` instead.
This commit is contained in:
JoelEinbinder 2017-10-17 15:35:00 -07:00 committed by Andrey Lushnikov
parent fbee98aa51
commit 2b7951473d
4 changed files with 58 additions and 27 deletions

View File

@ -14,6 +14,7 @@
* [puppeteer.launch([options])](#puppeteerlaunchoptions) * [puppeteer.launch([options])](#puppeteerlaunchoptions)
- [class: Browser](#class-browser) - [class: Browser](#class-browser)
* [browser.close()](#browserclose) * [browser.close()](#browserclose)
* [browser.disconnect()](#browserdisconnect)
* [browser.newPage()](#browsernewpage) * [browser.newPage()](#browsernewpage)
* [browser.version()](#browserversion) * [browser.version()](#browserversion)
* [browser.wsEndpoint()](#browserwsendpoint) * [browser.wsEndpoint()](#browserwsendpoint)
@ -259,10 +260,31 @@ puppeteer.launch().then(async browser => {
}); });
``` ```
An example of disconnecting from and reconnecting to a [Browser]:
```js
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
// Store the endpoint to be able to reconnect to Chromium
const browserWSEndpoint = browser.wsEndpoint();
// Disconnect puppeteer from Chromium
browser.disconnect();
// Use the endpoint to reestablish a connection
const browser2 = await puppeteer.connect({browserWSEndpoint});
// Close Chromium
await browser2.close();
});
```
#### browser.close() #### browser.close()
- returns: <[Promise]> - returns: <[Promise]>
Closes browser with all the pages (if any were opened). The browser object itself is considered to be disposed and can not be used anymore. Closes Chromium and all of its pages (if any were opened). The browser object itself is considered disposed and cannot be used anymore.
#### browser.disconnect()
Disconnects Puppeteer from the browser, but leaves the Chromium process running. After calling `disconnect`, the browser object is considered disposed and cannot be used anymore.
#### browser.newPage() #### browser.newPage()
- returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object. - returns: <[Promise]<[Page]>> Promise which resolves to a new [Page] object.

View File

@ -58,8 +58,12 @@ class Browser extends EventEmitter {
} }
async close() { async close() {
this._connection.dispose();
await this._closeCallback.call(null); await this._closeCallback.call(null);
this.disconnect();
}
disconnect() {
this._connection.dispose();
} }
} }

View File

@ -110,8 +110,10 @@ class Launcher {
chromeProcess.stderr.pipe(process.stderr); chromeProcess.stderr.pipe(process.stderr);
} }
let chromeClosed = false;
const waitForChromeToClose = new Promise((fulfill, reject) => { const waitForChromeToClose = new Promise((fulfill, reject) => {
chromeProcess.once('close', () => { chromeProcess.once('close', () => {
chromeClosed = true;
// Cleanup as processes exit. // Cleanup as processes exit.
if (temporaryUserDataDir) { if (temporaryUserDataDir) {
removeFolderAsync(temporaryUserDataDir) removeFolderAsync(temporaryUserDataDir)
@ -126,11 +128,12 @@ class Launcher {
const listeners = [ helper.addEventListener(process, 'exit', killChrome) ]; const listeners = [ helper.addEventListener(process, 'exit', killChrome) ];
if (options.handleSIGINT !== false) if (options.handleSIGINT !== false)
listeners.push(helper.addEventListener(process, 'SIGINT', killChrome)); listeners.push(helper.addEventListener(process, 'SIGINT', killChrome));
/** @type {?Connection} */
let connection = null;
try { try {
const connectionDelay = options.slowMo || 0; const connectionDelay = options.slowMo || 0;
const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, options.timeout || 30 * 1000); const browserWSEndpoint = await waitForWSEndpoint(chromeProcess, options.timeout || 30 * 1000);
const connection = await Connection.create(browserWSEndpoint, connectionDelay); connection = await Connection.create(browserWSEndpoint, connectionDelay);
return new Browser(connection, options, killChrome); return new Browser(connection, options, killChrome);
} catch (e) { } catch (e) {
killChrome(); killChrome();
@ -142,26 +145,21 @@ class Launcher {
*/ */
function killChrome() { function killChrome() {
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
if (chromeProcess.pid) { if (temporaryUserDataDir) {
if (temporaryUserDataDir) { if (chromeProcess.pid && !chromeProcess.killed && !chromeClosed) {
// Force kill chrome. // Force kill chrome.
if (process.platform === 'win32') if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`); childProcess.execSync(`taskkill /pid ${chromeProcess.pid} /T /F`);
else else
process.kill(-chromeProcess.pid, 'SIGKILL'); process.kill(-chromeProcess.pid, 'SIGKILL');
} else {
// Terminate chrome gracefully.
if (process.platform === 'win32')
childProcess.execSync(`taskkill /pid ${chromeProcess.pid}`);
else
process.kill(-chromeProcess.pid, 'SIGTERM');
} }
}
if (temporaryUserDataDir) {
// Attempt to remove temporary profile directory to avoid littering. // Attempt to remove temporary profile directory to avoid littering.
try { try {
removeFolder.sync(temporaryUserDataDir); removeFolder.sync(temporaryUserDataDir);
} catch (e) { } } catch (e) { }
} else if (connection) {
// Attempt to close chrome gracefully
connection.send('Browser.close');
} }
return waitForChromeToClose; return waitForChromeToClose;
} }
@ -181,7 +179,7 @@ class Launcher {
*/ */
static async connect(options = {}) { static async connect(options = {}) {
const connection = await Connection.create(options.browserWSEndpoint); const connection = await Connection.create(options.browserWSEndpoint);
return new Browser(connection, options); return new Browser(connection, options, () => connection.send('Browser.close'));
} }
} }

View File

@ -38,7 +38,6 @@ const EMPTY_PAGE = PREFIX + '/empty.html';
const HTTPS_PORT = 8908; const HTTPS_PORT = 8908;
const HTTPS_PREFIX = 'https://localhost:' + HTTPS_PORT; const HTTPS_PREFIX = 'https://localhost:' + HTTPS_PORT;
const windows = /^win/.test(process.platform);
const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true'; const headless = (process.env.HEADLESS || 'true').trim().toLowerCase() === 'true';
const slowMo = parseInt((process.env.SLOW_MO || '0').trim(), 10); const slowMo = parseInt((process.env.SLOW_MO || '0').trim(), 10);
const executablePath = process.env.CHROME; const executablePath = process.env.CHROME;
@ -109,9 +108,7 @@ describe('Puppeteer', function() {
await puppeteer.launch(options).catch(e => waitError = e); await puppeteer.launch(options).catch(e => waitError = e);
expect(waitError.message.startsWith('Failed to launch chrome! spawn random-invalid-path ENOENT')).toBe(true); expect(waitError.message.startsWith('Failed to launch chrome! spawn random-invalid-path ENOENT')).toBe(true);
})); }));
// Windows has issues running Chromium using a custom user data dir. It hangs when closing the browser. it('userDataDir option', SX(async function() {
// @see https://github.com/GoogleChrome/puppeteer/issues/918
(windows ? xit : it)('userDataDir option', SX(async function() {
const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir')); const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir'));
const options = Object.assign({userDataDir}, defaultBrowserOptions); const options = Object.assign({userDataDir}, defaultBrowserOptions);
const browser = await puppeteer.launch(options); const browser = await puppeteer.launch(options);
@ -120,9 +117,7 @@ describe('Puppeteer', function() {
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
rm(userDataDir); rm(userDataDir);
})); }));
// Windows has issues running Chromium using a custom user data dir. It hangs when closing the browser. it('userDataDir argument', SX(async function() {
// @see https://github.com/GoogleChrome/puppeteer/issues/918
(windows ? xit : it)('userDataDir argument', SX(async function() {
const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir')); const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir'));
const options = Object.assign({}, defaultBrowserOptions); const options = Object.assign({}, defaultBrowserOptions);
options.args = [`--user-data-dir=${userDataDir}`].concat(options.args); options.args = [`--user-data-dir=${userDataDir}`].concat(options.args);
@ -132,9 +127,7 @@ describe('Puppeteer', function() {
expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
rm(userDataDir); rm(userDataDir);
})); }));
// Headless has issues shutting down gracefully it('userDataDir option should restore state', SX(async function() {
// @see https://crbug.com/771830
(headless ? xit : it)('userDataDir option should restore state', SX(async function() {
const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir')); const userDataDir = fs.mkdtempSync(path.join(__dirname, 'test-user-data-dir'));
const options = Object.assign({userDataDir}, defaultBrowserOptions); const options = Object.assign({userDataDir}, defaultBrowserOptions);
const browser = await puppeteer.launch(options); const browser = await puppeteer.launch(options);
@ -170,14 +163,28 @@ describe('Puppeteer', function() {
})); }));
}); });
describe('Puppeteer.connect', function() { describe('Puppeteer.connect', function() {
it('should work', SX(async function() { it('should be able to connect multiple times to the same browser', SX(async function() {
const originalBrowser = await puppeteer.launch(defaultBrowserOptions); const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browser = await puppeteer.connect({ const browser = await puppeteer.connect({
browserWSEndpoint: originalBrowser.wsEndpoint() browserWSEndpoint: originalBrowser.wsEndpoint()
}); });
const page = await browser.newPage(); const page = await browser.newPage();
expect(await page.evaluate(() => 7 * 8)).toBe(56); expect(await page.evaluate(() => 7 * 8)).toBe(56);
originalBrowser.close(); browser.disconnect();
const secondPage = await originalBrowser.newPage();
expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work');
await originalBrowser.close();
}));
it('should be able to reconnect to a disconnected browser', SX(async function() {
const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
const browserWSEndpoint = originalBrowser.wsEndpoint();
originalBrowser.disconnect();
const browser = await puppeteer.connect({browserWSEndpoint});
const page = await browser.newPage();
expect(await page.evaluate(() => 7 * 8)).toBe(56);
await browser.close();
})); }));
}); });
describe('Puppeteer.executablePath', function() { describe('Puppeteer.executablePath', function() {