@crawlee/browser-pool 4.0.0-beta.19 → 4.0.0-beta.190
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/abstract-classes/browser-controller.d.ts +91 -14
- package/abstract-classes/browser-controller.js +61 -24
- package/abstract-classes/browser-plugin.d.ts +48 -17
- package/abstract-classes/browser-plugin.js +69 -13
- package/anonymize-proxy.d.ts +5 -2
- package/anonymize-proxy.js +9 -5
- package/browser-pool.d.ts +106 -29
- package/browser-pool.js +250 -87
- package/container-proxy-server.d.ts +0 -1
- package/container-proxy-server.js +0 -1
- package/events.d.ts +2 -3
- package/events.js +0 -1
- package/fingerprinting/hooks.d.ts +0 -1
- package/fingerprinting/hooks.js +26 -10
- package/fingerprinting/types.d.ts +3 -4
- package/fingerprinting/types.js +0 -1
- package/fingerprinting/utils.d.ts +0 -1
- package/fingerprinting/utils.js +5 -6
- package/index.d.ts +14 -7
- package/index.js +5 -3
- package/launch-context.d.ts +20 -5
- package/launch-context.js +21 -10
- package/package.json +9 -9
- package/playwright/playwright-browser.d.ts +2 -6
- package/playwright/playwright-browser.js +16 -17
- package/playwright/playwright-controller.d.ts +0 -1
- package/playwright/playwright-controller.js +9 -6
- package/playwright/playwright-plugin.d.ts +10 -7
- package/playwright/playwright-plugin.js +40 -16
- package/proxy-server.d.ts +0 -1
- package/proxy-server.js +0 -1
- package/puppeteer/puppeteer-controller.d.ts +0 -1
- package/puppeteer/puppeteer-controller.js +28 -20
- package/puppeteer/puppeteer-plugin.d.ts +5 -3
- package/puppeteer/puppeteer-plugin.js +83 -46
- package/remote-browser-pool.d.ts +170 -0
- package/remote-browser-pool.js +202 -0
- package/remote-browser-provider.d.ts +83 -0
- package/remote-browser-provider.js +67 -0
- package/utils.d.ts +7 -1
- package/utils.js +19 -1
- package/abstract-classes/browser-controller.d.ts.map +0 -1
- package/abstract-classes/browser-controller.js.map +0 -1
- package/abstract-classes/browser-plugin.d.ts.map +0 -1
- package/abstract-classes/browser-plugin.js.map +0 -1
- package/anonymize-proxy.d.ts.map +0 -1
- package/anonymize-proxy.js.map +0 -1
- package/browser-pool.d.ts.map +0 -1
- package/browser-pool.js.map +0 -1
- package/container-proxy-server.d.ts.map +0 -1
- package/container-proxy-server.js.map +0 -1
- package/events.d.ts.map +0 -1
- package/events.js.map +0 -1
- package/fingerprinting/hooks.d.ts.map +0 -1
- package/fingerprinting/hooks.js.map +0 -1
- package/fingerprinting/types.d.ts.map +0 -1
- package/fingerprinting/types.js.map +0 -1
- package/fingerprinting/utils.d.ts.map +0 -1
- package/fingerprinting/utils.js.map +0 -1
- package/index.d.ts.map +0 -1
- package/index.js.map +0 -1
- package/launch-context.d.ts.map +0 -1
- package/launch-context.js.map +0 -1
- package/logger.d.ts +0 -3
- package/logger.d.ts.map +0 -1
- package/logger.js +0 -5
- package/logger.js.map +0 -1
- package/playwright/playwright-browser.d.ts.map +0 -1
- package/playwright/playwright-browser.js.map +0 -1
- package/playwright/playwright-controller.d.ts.map +0 -1
- package/playwright/playwright-controller.js.map +0 -1
- package/playwright/playwright-plugin.d.ts.map +0 -1
- package/playwright/playwright-plugin.js.map +0 -1
- package/proxy-server.d.ts.map +0 -1
- package/proxy-server.js.map +0 -1
- package/puppeteer/puppeteer-controller.d.ts.map +0 -1
- package/puppeteer/puppeteer-controller.js.map +0 -1
- package/puppeteer/puppeteer-plugin.d.ts.map +0 -1
- package/puppeteer/puppeteer-plugin.js.map +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/browser-pool.js
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { SessionError, serviceLocator } from '@crawlee/core';
|
|
3
|
+
import { parseArgument, schemas } from '@crawlee/utils/internal';
|
|
1
4
|
import { FingerprintGenerator } from 'fingerprint-generator';
|
|
2
5
|
import { FingerprintInjector } from 'fingerprint-injector';
|
|
3
6
|
import { nanoid } from 'nanoid';
|
|
4
|
-
import ow from 'ow';
|
|
5
7
|
import pLimit from 'p-limit';
|
|
6
8
|
import QuickLRU from 'quick-lru';
|
|
7
9
|
import { TypedEmitter } from 'tiny-typed-emitter';
|
|
10
|
+
import { z } from 'zod';
|
|
8
11
|
import { addTimeoutToPromise, tryCancel } from '@apify/timeout';
|
|
12
|
+
import { BROWSER_POOL_EVENTS } from './events.js';
|
|
9
13
|
import { createFingerprintPreLaunchHook, createPostPageCreateHook, createPrePageCreateHook, } from './fingerprinting/hooks.js';
|
|
10
|
-
|
|
14
|
+
const PAGE_CLOSE_TIMEOUT_MILLIS = 5000;
|
|
11
15
|
const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000;
|
|
12
16
|
const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
17
|
+
const browserPoolOptionsSchema = z.strictObject({
|
|
18
|
+
browserPlugins: schemas.anyArray.refine((value) => value.length >= 1, 'Expected a non-empty array'),
|
|
19
|
+
maxOpenPagesPerBrowser: schemas.anyNumber.default(20),
|
|
20
|
+
retireBrowserAfterPageCount: schemas.anyNumber.default(100),
|
|
21
|
+
operationTimeoutSecs: schemas.anyNumber.default(15),
|
|
22
|
+
closeInactiveBrowserAfterSecs: schemas.anyNumber.default(300),
|
|
23
|
+
retireInactiveBrowserAfterSecs: schemas.anyNumber.default(10),
|
|
24
|
+
preLaunchHooks: schemas.anyArray.default(() => []),
|
|
25
|
+
postLaunchHooks: schemas.anyArray.default(() => []),
|
|
26
|
+
prePageCreateHooks: schemas.anyArray.default(() => []),
|
|
27
|
+
postPageCreateHooks: schemas.anyArray.default(() => []),
|
|
28
|
+
prePageCloseHooks: schemas.anyArray.default(() => []),
|
|
29
|
+
postPageCloseHooks: schemas.anyArray.default(() => []),
|
|
30
|
+
useFingerprints: z.boolean().default(true),
|
|
31
|
+
fingerprintOptions: schemas.anyObject.default(() => ({})),
|
|
32
|
+
});
|
|
13
33
|
/**
|
|
14
34
|
* The `BrowserPool` class is the most important class of the `browser-pool` module.
|
|
15
35
|
* It manages opening and closing of browsers and their pages and its constructor
|
|
@@ -60,6 +80,7 @@ const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
|
60
80
|
export class BrowserPool extends TypedEmitter {
|
|
61
81
|
browserPlugins;
|
|
62
82
|
maxOpenPagesPerBrowser;
|
|
83
|
+
maxOpenBrowsers;
|
|
63
84
|
retireBrowserAfterPageCount;
|
|
64
85
|
operationTimeoutMillis;
|
|
65
86
|
closeInactiveBrowserAfterMillis;
|
|
@@ -81,29 +102,14 @@ export class BrowserPool extends TypedEmitter {
|
|
|
81
102
|
fingerprintInjector;
|
|
82
103
|
fingerprintGenerator;
|
|
83
104
|
fingerprintCache;
|
|
84
|
-
browserKillerInterval
|
|
85
|
-
browserRetireInterval;
|
|
86
|
-
limiter = pLimit(1);
|
|
105
|
+
browserKillerInterval;
|
|
106
|
+
#browserRetireInterval;
|
|
107
|
+
#limiter = pLimit(1);
|
|
108
|
+
#log;
|
|
87
109
|
constructor(options) {
|
|
88
110
|
super();
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
browserPlugins: ow.array.minLength(1),
|
|
92
|
-
maxOpenPagesPerBrowser: ow.optional.number,
|
|
93
|
-
retireBrowserAfterPageCount: ow.optional.number,
|
|
94
|
-
operationTimeoutSecs: ow.optional.number,
|
|
95
|
-
closeInactiveBrowserAfterSecs: ow.optional.number,
|
|
96
|
-
retireInactiveBrowserAfterSecs: ow.optional.number,
|
|
97
|
-
preLaunchHooks: ow.optional.array,
|
|
98
|
-
postLaunchHooks: ow.optional.array,
|
|
99
|
-
prePageCreateHooks: ow.optional.array,
|
|
100
|
-
postPageCreateHooks: ow.optional.array,
|
|
101
|
-
prePageCloseHooks: ow.optional.array,
|
|
102
|
-
postPageCloseHooks: ow.optional.array,
|
|
103
|
-
useFingerprints: ow.optional.boolean,
|
|
104
|
-
fingerprintOptions: ow.optional.object,
|
|
105
|
-
}));
|
|
106
|
-
const { browserPlugins, maxOpenPagesPerBrowser = 20, retireBrowserAfterPageCount = 100, operationTimeoutSecs = 15, closeInactiveBrowserAfterSecs = 300, retireInactiveBrowserAfterSecs = 10, preLaunchHooks = [], postLaunchHooks = [], prePageCreateHooks = [], postPageCreateHooks = [], prePageCloseHooks = [], postPageCloseHooks = [], useFingerprints = true, fingerprintOptions = {}, } = options;
|
|
111
|
+
this.#log = serviceLocator.getLogger().child({ prefix: 'BrowserPool' });
|
|
112
|
+
const { browserPlugins, maxOpenPagesPerBrowser, retireBrowserAfterPageCount, operationTimeoutSecs, closeInactiveBrowserAfterSecs, retireInactiveBrowserAfterSecs, preLaunchHooks, postLaunchHooks, prePageCreateHooks, postPageCreateHooks, prePageCloseHooks, postPageCloseHooks, useFingerprints, fingerprintOptions, } = parseArgument(options, browserPoolOptionsSchema);
|
|
107
113
|
const firstPluginConstructor = browserPlugins[0].constructor;
|
|
108
114
|
for (let i = 1; i < browserPlugins.length; i++) {
|
|
109
115
|
const providedPlugin = browserPlugins[i];
|
|
@@ -115,18 +121,23 @@ export class BrowserPool extends TypedEmitter {
|
|
|
115
121
|
}
|
|
116
122
|
this.browserPlugins = browserPlugins;
|
|
117
123
|
this.maxOpenPagesPerBrowser = maxOpenPagesPerBrowser;
|
|
124
|
+
this.maxOpenBrowsers = Infinity;
|
|
118
125
|
this.retireBrowserAfterPageCount = retireBrowserAfterPageCount;
|
|
119
126
|
this.operationTimeoutMillis = operationTimeoutSecs * 1000;
|
|
120
127
|
this.closeInactiveBrowserAfterMillis = closeInactiveBrowserAfterSecs * 1000;
|
|
128
|
+
// Sweeping slower than the window it enforces would round any sub-10s
|
|
129
|
+
// `closeInactiveBrowserAfterSecs` up to the sweep period.
|
|
130
|
+
this.browserKillerInterval = setInterval(async () => this.closeInactiveRetiredBrowsers(), Math.min(BROWSER_KILLER_INTERVAL_MILLIS, this.closeInactiveBrowserAfterMillis));
|
|
131
|
+
this.browserKillerInterval.unref();
|
|
121
132
|
this.useFingerprints = useFingerprints;
|
|
122
133
|
this.fingerprintOptions = fingerprintOptions;
|
|
123
|
-
this
|
|
134
|
+
this.#browserRetireInterval = setInterval(async () => this.activeBrowserControllers.forEach((controller) => {
|
|
124
135
|
if (controller.activePages === 0 &&
|
|
125
136
|
controller.lastPageOpenedAt < Date.now() - retireInactiveBrowserAfterSecs * 1000) {
|
|
126
137
|
this.retireBrowserController(controller);
|
|
127
138
|
}
|
|
128
139
|
}), retireInactiveBrowserAfterSecs * 1000);
|
|
129
|
-
this
|
|
140
|
+
this.#browserRetireInterval.unref();
|
|
130
141
|
// hooks
|
|
131
142
|
this.preLaunchHooks = preLaunchHooks;
|
|
132
143
|
this.postLaunchHooks = postLaunchHooks;
|
|
@@ -136,30 +147,56 @@ export class BrowserPool extends TypedEmitter {
|
|
|
136
147
|
this.postPageCloseHooks = postPageCloseHooks;
|
|
137
148
|
// fingerprinting
|
|
138
149
|
if (this.useFingerprints) {
|
|
139
|
-
this.
|
|
150
|
+
this.initializeFingerprinting();
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
/**
|
|
143
154
|
* Opens a new page in one of the running browsers or launches
|
|
144
155
|
* a new browser and opens a page there, if no browsers are active,
|
|
145
156
|
* or their page limits have been exceeded.
|
|
157
|
+
*
|
|
158
|
+
* **Session injection (best-effort):** When a {@link NewPageOptions.session|session} is
|
|
159
|
+
* provided, this implementation uses it as a cache key for browser fingerprints (when
|
|
160
|
+
* fingerprinting is enabled) and reads
|
|
161
|
+
* {@link ProxyInfo.url|session.proxyInfo.url} /
|
|
162
|
+
* {@link ProxyInfo.ignoreTlsErrors|session.proxyInfo.ignoreTlsErrors} as defaults
|
|
163
|
+
* for `proxyUrl` and `ignoreTlsErrors` respectively. Explicit `proxyUrl` /
|
|
164
|
+
* `ignoreTlsErrors` values in the options take precedence.
|
|
165
|
+
*
|
|
166
|
+
* Beyond fingerprint caching and proxy configuration, no other session
|
|
167
|
+
* properties are consumed — cookie and header injection remain the
|
|
168
|
+
* crawler's responsibility.
|
|
146
169
|
*/
|
|
147
170
|
async newPage(options = {}) {
|
|
148
|
-
const { id = nanoid(), pageOptions, browserPlugin = this.
|
|
171
|
+
const { id = nanoid(), pageOptions, browserPlugin = this.pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
|
|
149
172
|
if (this.pages.has(id)) {
|
|
150
173
|
throw new Error(`Page with ID: ${id} already exists.`);
|
|
151
174
|
}
|
|
152
175
|
if (browserPlugin && !this.browserPlugins.includes(browserPlugin)) {
|
|
153
176
|
throw new Error('Provided browserPlugin is not one of the plugins used by BrowserPool.');
|
|
154
177
|
}
|
|
178
|
+
// Bind the limiter callback to the current async-hooks context. p-limit
|
|
179
|
+
// otherwise resumes queued callbacks in the previous task's
|
|
180
|
+
// AsyncLocalStorage context, leaking aborted cancelTasks across unrelated
|
|
181
|
+
// requests (https://github.com/apify/crawlee/issues/3670). Mirrors the
|
|
182
|
+
// fix p-limit landed upstream in v5 (sindresorhus/p-limit#71); v5 is an
|
|
183
|
+
// ESM-only rewrite, so we can't bump it in Crawlee v3.
|
|
184
|
+
// Besides the cancelTask leak, the wrapper also keeps the per-request *storage transaction*
|
|
185
|
+
// ALS-scoped: without it, a queued callback would resume in the previous request's async
|
|
186
|
+
// context and run request B's storage writes inside request A's transaction.
|
|
187
|
+
// TODO(crawlee@v4): bump p-limit to v5 and drop this AsyncResource.bind wrapper.
|
|
155
188
|
// Limiter is necessary - https://github.com/apify/crawlee/issues/1126
|
|
156
|
-
return this
|
|
157
|
-
let browserController = this.
|
|
189
|
+
return this.#limiter(AsyncResource.bind(async () => {
|
|
190
|
+
let browserController = this.pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
|
|
158
191
|
if (!browserController)
|
|
159
|
-
browserController = await this.
|
|
192
|
+
browserController = await this.launchBrowser(id, {
|
|
193
|
+
browserPlugin,
|
|
194
|
+
proxyUrl,
|
|
195
|
+
ignoreTlsErrors,
|
|
196
|
+
});
|
|
160
197
|
tryCancel();
|
|
161
|
-
return await this.
|
|
162
|
-
});
|
|
198
|
+
return await this.createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
|
|
199
|
+
}));
|
|
163
200
|
}
|
|
164
201
|
/**
|
|
165
202
|
* Unlike {@link newPage}, `newPageInNewBrowser` always launches a new
|
|
@@ -167,13 +204,13 @@ export class BrowserPool extends TypedEmitter {
|
|
|
167
204
|
* configure the new browser.
|
|
168
205
|
*/
|
|
169
206
|
async newPageInNewBrowser(options = {}) {
|
|
170
|
-
const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this.
|
|
207
|
+
const { id = nanoid(), pageOptions, launchOptions, browserPlugin = this.pickBrowserPlugin() } = options;
|
|
171
208
|
if (this.pages.has(id)) {
|
|
172
209
|
throw new Error(`Page with ID: ${id} already exists.`);
|
|
173
210
|
}
|
|
174
|
-
const browserController = await this.
|
|
211
|
+
const browserController = await this.launchBrowser(id, { launchOptions, browserPlugin });
|
|
175
212
|
tryCancel();
|
|
176
|
-
return await this.
|
|
213
|
+
return await this.createPageForBrowser(id, browserController, pageOptions);
|
|
177
214
|
}
|
|
178
215
|
/**
|
|
179
216
|
* Opens new pages with all available plugins and returns an array
|
|
@@ -239,17 +276,22 @@ export class BrowserPool extends TypedEmitter {
|
|
|
239
276
|
getPageId(page) {
|
|
240
277
|
return this.pageIds.get(page);
|
|
241
278
|
}
|
|
242
|
-
async
|
|
279
|
+
async createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
|
|
243
280
|
// This is needed for concurrent newPage calls to wait for the browser launch.
|
|
244
281
|
// It's not ideal though, we need to come up with a better API.
|
|
245
|
-
|
|
246
|
-
await browserController['isActivePromise'];
|
|
282
|
+
await browserController.waitForActive();
|
|
247
283
|
tryCancel();
|
|
248
284
|
const finalPageOptions = browserController.launchContext.useIncognitoPages ? pageOptions : undefined;
|
|
249
285
|
if (finalPageOptions) {
|
|
250
286
|
Object.assign(finalPageOptions, browserController.normalizeProxyOptions(proxyUrl, pageOptions));
|
|
287
|
+
if (ignoreTlsErrors) {
|
|
288
|
+
Object.assign(finalPageOptions, {
|
|
289
|
+
ignoreHTTPSErrors: true,
|
|
290
|
+
acceptInsecureCerts: true,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
251
293
|
}
|
|
252
|
-
await this.
|
|
294
|
+
await this.executeHooks(this.prePageCreateHooks, pageId, browserController, finalPageOptions);
|
|
253
295
|
tryCancel();
|
|
254
296
|
let page;
|
|
255
297
|
try {
|
|
@@ -262,15 +304,15 @@ export class BrowserPool extends TypedEmitter {
|
|
|
262
304
|
if (browserController.totalPages >= this.retireBrowserAfterPageCount) {
|
|
263
305
|
this.retireBrowserController(browserController);
|
|
264
306
|
}
|
|
265
|
-
this.
|
|
307
|
+
this.overridePageClose(page);
|
|
266
308
|
}
|
|
267
309
|
catch (err) {
|
|
268
310
|
this.retireBrowserController(browserController);
|
|
269
311
|
throw new Error(`browserController.newPage() failed: ${browserController.id}\nCause:${err.message}.`);
|
|
270
312
|
}
|
|
271
|
-
await this.
|
|
313
|
+
await this.executeHooks(this.postPageCreateHooks, page, browserController);
|
|
272
314
|
tryCancel();
|
|
273
|
-
this.emit(
|
|
315
|
+
this.emit(BROWSER_POOL_EVENTS.PAGE_CREATED, page);
|
|
274
316
|
return page;
|
|
275
317
|
}
|
|
276
318
|
/**
|
|
@@ -285,7 +327,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
285
327
|
if (hasBeenRetiredOrKilled)
|
|
286
328
|
return;
|
|
287
329
|
this.retiredBrowserControllers.add(browserController);
|
|
288
|
-
this.emit(
|
|
330
|
+
this.emit(BROWSER_POOL_EVENTS.BROWSER_RETIRED, browserController);
|
|
289
331
|
this.startingBrowserControllers.delete(browserController);
|
|
290
332
|
this.activeBrowserControllers.delete(browserController);
|
|
291
333
|
}
|
|
@@ -298,6 +340,61 @@ export class BrowserPool extends TypedEmitter {
|
|
|
298
340
|
if (browserController)
|
|
299
341
|
this.retireBrowserController(browserController);
|
|
300
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Releases a page back to the pool. The page is closed and, if the
|
|
345
|
+
* optional `error` is a {@link SessionError}, the browser controller
|
|
346
|
+
* that served the page is retired so that its tainted state (cookies,
|
|
347
|
+
* storage, etc.) cannot leak into future sessions.
|
|
348
|
+
*
|
|
349
|
+
* This is the primary way the crawler should return pages to the pool.
|
|
350
|
+
*
|
|
351
|
+
* @param page The page to release.
|
|
352
|
+
* @param options.error The error that caused the page to be released, if any.
|
|
353
|
+
*/
|
|
354
|
+
async closePage(page, options) {
|
|
355
|
+
if (options?.error instanceof SessionError) {
|
|
356
|
+
this.retireBrowserByPage(page);
|
|
357
|
+
}
|
|
358
|
+
// The bound on this lives in `overridePageClose`, where it covers the pool's own
|
|
359
|
+
// bookkeeping as well. Wrapping it here instead would time out the override and abandon
|
|
360
|
+
// that bookkeeping, leaving the browser holding a slot it can never get back.
|
|
361
|
+
await page.close();
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Extracts the relevant state (currently just cookies) from a page via its
|
|
365
|
+
* owning {@link BrowserController}. Returns empty state when the page is
|
|
366
|
+
* no longer associated with a controller.
|
|
367
|
+
*
|
|
368
|
+
* As with {@link BrowserPool.injectPageState}, cookies are isolated per
|
|
369
|
+
* page only when the pool is configured with `useIncognitoPages: true`.
|
|
370
|
+
* With the default `useIncognitoPages: false`, the extracted cookies
|
|
371
|
+
* include those set by any sibling page sharing the same browser.
|
|
372
|
+
*/
|
|
373
|
+
async extractPageState(page) {
|
|
374
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
375
|
+
if (!controller) {
|
|
376
|
+
return { cookies: [] };
|
|
377
|
+
}
|
|
378
|
+
return { cookies: await controller.getCookies(page) };
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Injects state into a page via its owning {@link BrowserController}.
|
|
382
|
+
*
|
|
383
|
+
* No-op when the page is no longer associated with a controller.
|
|
384
|
+
*
|
|
385
|
+
* Note that cookies are isolated per page only when the pool is configured
|
|
386
|
+
* with `useIncognitoPages: true` — each page then gets its own browser
|
|
387
|
+
* context. With the default `useIncognitoPages: false`, all pages in a
|
|
388
|
+
* browser share a single context, so injected cookies are visible to every
|
|
389
|
+
* page served by that browser.
|
|
390
|
+
*/
|
|
391
|
+
async injectPageState(page, state) {
|
|
392
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
393
|
+
if (!controller) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
await controller.setCookies(page, state.cookies);
|
|
397
|
+
}
|
|
301
398
|
/**
|
|
302
399
|
* Removes all active browsers from the pool. The browsers will be
|
|
303
400
|
* closed after all their pages are closed.
|
|
@@ -312,50 +409,66 @@ export class BrowserPool extends TypedEmitter {
|
|
|
312
409
|
* @return {Promise<void>}
|
|
313
410
|
*/
|
|
314
411
|
async closeAllBrowsers() {
|
|
315
|
-
const controllers = this.
|
|
412
|
+
const controllers = this.getAllBrowserControllers();
|
|
316
413
|
const promises = [...controllers]
|
|
317
414
|
.filter((controller) => controller.isActive)
|
|
318
415
|
.map(async (controller) => controller.close());
|
|
319
416
|
await Promise.all(promises);
|
|
320
417
|
}
|
|
418
|
+
async [Symbol.asyncDispose]() {
|
|
419
|
+
await this.destroy();
|
|
420
|
+
}
|
|
321
421
|
/**
|
|
322
|
-
* Closes
|
|
422
|
+
* Closes every managed browser and empties the pool, which stays usable afterwards — a crawler releases its
|
|
423
|
+
* browsers when a run ends and may start another run on the same pool.
|
|
323
424
|
*/
|
|
324
|
-
async
|
|
325
|
-
clearInterval(this.browserKillerInterval);
|
|
326
|
-
clearInterval(this.browserRetireInterval);
|
|
327
|
-
this.browserKillerInterval = undefined;
|
|
328
|
-
this.browserRetireInterval = undefined;
|
|
425
|
+
async releaseAllBrowsers() {
|
|
329
426
|
await this.closeAllBrowsers();
|
|
330
|
-
this._teardown();
|
|
331
|
-
}
|
|
332
|
-
_teardown() {
|
|
333
427
|
this.startingBrowserControllers.clear();
|
|
334
428
|
this.activeBrowserControllers.clear();
|
|
335
429
|
this.retiredBrowserControllers.clear();
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Closes all managed browsers and tears the pool down for good: its intervals are cleared without being
|
|
433
|
+
* re-armed and its listeners are dropped, so it cannot be used again.
|
|
434
|
+
*/
|
|
435
|
+
async destroy() {
|
|
436
|
+
clearInterval(this.browserKillerInterval);
|
|
437
|
+
clearInterval(this.#browserRetireInterval);
|
|
438
|
+
this.browserKillerInterval = undefined;
|
|
439
|
+
this.#browserRetireInterval = undefined;
|
|
440
|
+
await this.releaseAllBrowsers();
|
|
336
441
|
this.removeAllListeners();
|
|
337
442
|
}
|
|
338
|
-
|
|
443
|
+
getAllBrowserControllers() {
|
|
339
444
|
return new Set([
|
|
340
445
|
...this.startingBrowserControllers,
|
|
341
446
|
...this.activeBrowserControllers,
|
|
342
447
|
...this.retiredBrowserControllers,
|
|
343
448
|
]);
|
|
344
449
|
}
|
|
345
|
-
async
|
|
346
|
-
const { browserPlugin, launchOptions,
|
|
450
|
+
async launchBrowser(pageId, options) {
|
|
451
|
+
const { browserPlugin, launchOptions, proxyUrl, ignoreTlsErrors } = options;
|
|
347
452
|
const browserController = browserPlugin.createController();
|
|
348
453
|
this.startingBrowserControllers.add(browserController);
|
|
349
454
|
const launchContext = browserPlugin.createLaunchContext({
|
|
350
455
|
id: pageId,
|
|
351
456
|
launchOptions,
|
|
352
|
-
proxyTier,
|
|
353
457
|
proxyUrl,
|
|
354
458
|
});
|
|
459
|
+
// Disable SSL verification for MITM proxies
|
|
460
|
+
if (ignoreTlsErrors) {
|
|
461
|
+
/**
|
|
462
|
+
* @see https://playwright.dev/docs/api/class-browser/#browser-new-context
|
|
463
|
+
* @see https://github.com/puppeteer/puppeteer/blob/main/docs/api.md
|
|
464
|
+
*/
|
|
465
|
+
launchContext.launchOptions.ignoreHTTPSErrors = true;
|
|
466
|
+
launchContext.launchOptions.acceptInsecureCerts = true;
|
|
467
|
+
}
|
|
355
468
|
try {
|
|
356
469
|
// If the hooks or the launch fails, we need to delete the controller,
|
|
357
470
|
// because otherwise it would be stuck in limbo without a browser.
|
|
358
|
-
await this.
|
|
471
|
+
await this.executeHooks(this.preLaunchHooks, pageId, launchContext);
|
|
359
472
|
tryCancel();
|
|
360
473
|
const browser = await browserPlugin.launch(launchContext);
|
|
361
474
|
tryCancel();
|
|
@@ -365,18 +478,17 @@ export class BrowserPool extends TypedEmitter {
|
|
|
365
478
|
this.startingBrowserControllers.delete(browserController);
|
|
366
479
|
throw err;
|
|
367
480
|
}
|
|
368
|
-
log.debug('Launched new browser.', { id: browserController.id });
|
|
369
|
-
browserController.proxyTier = proxyTier;
|
|
481
|
+
this.#log.debug('Launched new browser.', { id: browserController.id });
|
|
370
482
|
browserController.proxyUrl = proxyUrl;
|
|
371
483
|
try {
|
|
372
484
|
// If the launch fails on the post-launch hooks, we need to clean up
|
|
373
485
|
// both the controller and the browser before throwing.
|
|
374
|
-
await this.
|
|
486
|
+
await this.executeHooks(this.postLaunchHooks, pageId, browserController);
|
|
375
487
|
}
|
|
376
488
|
catch (err) {
|
|
377
489
|
this.startingBrowserControllers.delete(browserController);
|
|
378
490
|
browserController.close().catch((closeErr) => {
|
|
379
|
-
log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
491
|
+
this.#log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
380
492
|
id: browserController.id,
|
|
381
493
|
});
|
|
382
494
|
});
|
|
@@ -386,33 +498,31 @@ export class BrowserPool extends TypedEmitter {
|
|
|
386
498
|
browserController.activate();
|
|
387
499
|
this.startingBrowserControllers.delete(browserController);
|
|
388
500
|
this.activeBrowserControllers.add(browserController);
|
|
389
|
-
this.emit(
|
|
501
|
+
this.emit(BROWSER_POOL_EVENTS.BROWSER_LAUNCHED, browserController);
|
|
390
502
|
return browserController;
|
|
391
503
|
}
|
|
392
504
|
/**
|
|
393
505
|
* Picks plugins round robin.
|
|
394
506
|
* @private
|
|
395
507
|
*/
|
|
396
|
-
|
|
508
|
+
pickBrowserPlugin() {
|
|
397
509
|
const pluginIndex = this.pageCounter % this.browserPlugins.length;
|
|
398
510
|
this.pageCounter++;
|
|
399
511
|
return this.browserPlugins[pluginIndex];
|
|
400
512
|
}
|
|
401
|
-
|
|
513
|
+
pickBrowserWithFreeCapacity(browserPlugin, options) {
|
|
402
514
|
return [...this.activeBrowserControllers].find((controller) => {
|
|
403
515
|
const hasCapacity = controller.activePages < this.maxOpenPagesPerBrowser;
|
|
404
516
|
const isCorrectPlugin = controller.browserPlugin === browserPlugin;
|
|
405
517
|
const isSameProxyUrl = controller.proxyUrl === options?.proxyUrl;
|
|
406
|
-
const isCorrectProxyTier = controller.proxyTier === options?.proxyTier;
|
|
407
518
|
return (isCorrectPlugin &&
|
|
408
519
|
hasCapacity &&
|
|
409
|
-
(
|
|
410
|
-
(options?.proxyTier && isCorrectProxyTier) ||
|
|
520
|
+
(!controller.launchContext.browserPerProxy ||
|
|
411
521
|
(options?.proxyUrl && isSameProxyUrl) ||
|
|
412
|
-
(!options?.proxyUrl && !
|
|
522
|
+
(!options?.proxyUrl && !controller.proxyUrl)));
|
|
413
523
|
});
|
|
414
524
|
}
|
|
415
|
-
async
|
|
525
|
+
async closeInactiveRetiredBrowsers() {
|
|
416
526
|
const closedBrowserIds = [];
|
|
417
527
|
for (const controller of this.retiredBrowserControllers) {
|
|
418
528
|
const millisSinceLastPageOpened = Date.now() - controller.lastPageOpenedAt;
|
|
@@ -420,61 +530,115 @@ export class BrowserPool extends TypedEmitter {
|
|
|
420
530
|
const isBrowserEmpty = controller.activePages === 0;
|
|
421
531
|
if (isBrowserIdle || isBrowserEmpty) {
|
|
422
532
|
const { id } = controller;
|
|
423
|
-
log.debug('Closing retired browser.', { id });
|
|
533
|
+
this.#log.debug('Closing retired browser.', { id });
|
|
424
534
|
await controller.close();
|
|
425
535
|
this.retiredBrowserControllers.delete(controller);
|
|
426
536
|
closedBrowserIds.push(id);
|
|
427
537
|
}
|
|
428
538
|
}
|
|
429
539
|
if (closedBrowserIds.length) {
|
|
430
|
-
log.debug('Closed retired browsers.', {
|
|
540
|
+
this.#log.debug('Closed retired browsers.', {
|
|
431
541
|
count: closedBrowserIds.length,
|
|
432
542
|
closedBrowserIds,
|
|
433
543
|
});
|
|
434
544
|
}
|
|
435
545
|
}
|
|
436
|
-
|
|
546
|
+
overridePageClose(page) {
|
|
437
547
|
const originalPageClose = page.close;
|
|
438
548
|
const browserController = this.pageToBrowserController.get(page);
|
|
439
549
|
const pageId = this.getPageId(page);
|
|
440
550
|
page.close = async (...args) => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
551
|
+
// A browser can acknowledge the close and then never destroy the target, in which case
|
|
552
|
+
// this never settles and the page's own `close` event never fires either (Chromium
|
|
553
|
+
// 536385539). Bound the whole sequence so nothing here can hang the caller, then
|
|
554
|
+
// reconcile the pool regardless of the outcome, so a page that refuses to close cannot
|
|
555
|
+
// leave the pool believing it is still open. `Promise.race` rather than
|
|
556
|
+
// `addTimeoutToPromise`: the latter inherits its AbortController from the calling frame,
|
|
557
|
+
// so a timeout here would cancel the task of whoever awaited `page.close()`.
|
|
558
|
+
let pageClosed = false;
|
|
559
|
+
const closing = (async () => {
|
|
560
|
+
await this.executeHooks(this.prePageCloseHooks, page, browserController);
|
|
561
|
+
await originalPageClose.apply(page, args).catch((err) => {
|
|
562
|
+
this.#log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
|
|
563
|
+
});
|
|
564
|
+
// Whether the page itself is gone. Tracked separately from the sequence finishing,
|
|
565
|
+
// so that a slow hook does not get the browser retired.
|
|
566
|
+
pageClosed = true;
|
|
567
|
+
await this.executeHooks(this.postPageCloseHooks, pageId, browserController);
|
|
568
|
+
})();
|
|
569
|
+
let timeout;
|
|
570
|
+
const finished = await Promise.race([
|
|
571
|
+
closing.then(() => true, (err) => {
|
|
572
|
+
this.#log.warning(`Closing a page failed, releasing it from the pool anyway.\nCause:${err.message}`, { id: browserController.id, pageId });
|
|
573
|
+
return true;
|
|
574
|
+
}),
|
|
575
|
+
new Promise((resolve) => {
|
|
576
|
+
timeout = setTimeout(() => resolve(false), PAGE_CLOSE_TIMEOUT_MILLIS);
|
|
577
|
+
}),
|
|
578
|
+
]);
|
|
579
|
+
clearTimeout(timeout);
|
|
580
|
+
if (!finished) {
|
|
581
|
+
this.#log.warning(`Closing a page did not finish within ${PAGE_CLOSE_TIMEOUT_MILLIS / 1000} seconds, ` +
|
|
582
|
+
'releasing it from the pool anyway.', { id: browserController.id, pageId });
|
|
583
|
+
}
|
|
584
|
+
if (!pageClosed) {
|
|
585
|
+
// The page is still attached, so this browser cannot be trusted with more work.
|
|
586
|
+
// Retiring it lets the reclamation below close the process once its pages drain.
|
|
587
|
+
this.retireBrowserController(browserController);
|
|
588
|
+
}
|
|
589
|
+
browserController.registerPageClosed(page);
|
|
446
590
|
this.pages.delete(pageId);
|
|
447
|
-
this.
|
|
448
|
-
this.emit(
|
|
591
|
+
this.closeRetiredBrowserWithNoPages(browserController);
|
|
592
|
+
this.emit(BROWSER_POOL_EVENTS.PAGE_CLOSED, page);
|
|
449
593
|
};
|
|
450
594
|
}
|
|
451
|
-
async
|
|
595
|
+
async executeHooks(hooks, ...args) {
|
|
452
596
|
for (const hook of hooks) {
|
|
453
597
|
await hook(...args);
|
|
454
598
|
}
|
|
455
599
|
}
|
|
456
|
-
|
|
600
|
+
closeRetiredBrowserWithNoPages(browserController) {
|
|
457
601
|
if (browserController.activePages === 0 && this.retiredBrowserControllers.has(browserController)) {
|
|
458
602
|
// Run this with a delay, otherwise page.close()
|
|
459
603
|
// might fail with "Protocol error (Target.closeTarget): Target closed."
|
|
460
604
|
setTimeout(() => {
|
|
461
|
-
log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
605
|
+
this.#log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
462
606
|
void browserController.close().finally(() => {
|
|
463
607
|
this.retiredBrowserControllers.delete(browserController);
|
|
464
608
|
});
|
|
465
609
|
}, PAGE_CLOSE_KILL_TIMEOUT_MILLIS);
|
|
466
610
|
}
|
|
467
611
|
}
|
|
468
|
-
|
|
612
|
+
/**
|
|
613
|
+
* Returns `true` if the pool can accept a new browser launch without exceeding
|
|
614
|
+
* {@link BrowserPoolOptions.maxOpenBrowsers}. Counts starting, active, and retired browsers.
|
|
615
|
+
*/
|
|
616
|
+
hasFreeBrowserSlot() {
|
|
617
|
+
const total = this.startingBrowserControllers.size +
|
|
618
|
+
this.activeBrowserControllers.size +
|
|
619
|
+
this.retiredBrowserControllers.size;
|
|
620
|
+
return total < this.maxOpenBrowsers;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Returns `true` if any active browser has room for another page.
|
|
624
|
+
*/
|
|
625
|
+
hasActiveBrowserWithFreeCapacity() {
|
|
626
|
+
for (const controller of this.activeBrowserControllers) {
|
|
627
|
+
if (controller.activePages < this.maxOpenPagesPerBrowser)
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
initializeFingerprinting() {
|
|
469
633
|
const { useFingerprintCache = true, fingerprintCacheSize = 10_000 } = this.fingerprintOptions;
|
|
470
634
|
this.fingerprintGenerator = new FingerprintGenerator(this.fingerprintOptions.fingerprintGeneratorOptions);
|
|
471
635
|
this.fingerprintInjector = new FingerprintInjector();
|
|
472
636
|
if (useFingerprintCache) {
|
|
473
637
|
this.fingerprintCache = new QuickLRU({ maxSize: fingerprintCacheSize });
|
|
474
638
|
}
|
|
475
|
-
this.
|
|
639
|
+
this.addFingerprintHooks();
|
|
476
640
|
}
|
|
477
|
-
|
|
641
|
+
addFingerprintHooks() {
|
|
478
642
|
this.preLaunchHooks = [
|
|
479
643
|
...this.preLaunchHooks,
|
|
480
644
|
// This is flipped because of the fingerprint cache.
|
|
@@ -485,4 +649,3 @@ export class BrowserPool extends TypedEmitter {
|
|
|
485
649
|
this.postPageCreateHooks = [createPostPageCreateHook(this.fingerprintInjector), ...this.postPageCreateHooks];
|
|
486
650
|
}
|
|
487
651
|
}
|
|
488
|
-
//# sourceMappingURL=browser-pool.js.map
|
package/events.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare enum BROWSER_POOL_EVENTS {
|
|
2
2
|
BROWSER_LAUNCHED = "browserLaunched",
|
|
3
3
|
BROWSER_RETIRED = "browserRetired",
|
|
4
4
|
BROWSER_CLOSED = "browserClosed",
|
|
5
5
|
PAGE_CREATED = "pageCreated",
|
|
6
6
|
PAGE_CLOSED = "pageClosed"
|
|
7
7
|
}
|
|
8
|
-
export declare
|
|
8
|
+
export declare enum BROWSER_CONTROLLER_EVENTS {
|
|
9
9
|
BROWSER_CLOSED = "browserClosed"
|
|
10
10
|
}
|
|
11
|
-
//# sourceMappingURL=events.d.ts.map
|
package/events.js
CHANGED
|
@@ -14,4 +14,3 @@ export declare function createPrePageCreateHook(): (_pageId: string, browserCont
|
|
|
14
14
|
* @internal
|
|
15
15
|
*/
|
|
16
16
|
export declare function createPostPageCreateHook(fingerprintInjector: FingerprintInjector): (page: any, browserController: BrowserController) => Promise<void>;
|
|
17
|
-
//# sourceMappingURL=hooks.d.ts.map
|