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:
parent
fbee98aa51
commit
2b7951473d
24
docs/api.md
24
docs/api.md
@ -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.
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
test/test.js
31
test/test.js
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user