@crawlee/browser-pool 4.0.0-beta.8 → 4.0.0-beta.80
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 +17 -13
- package/abstract-classes/browser-controller.d.ts +67 -8
- package/abstract-classes/browser-controller.js +8 -11
- package/abstract-classes/browser-plugin.d.ts +38 -3
- package/abstract-classes/browser-plugin.js +65 -7
- package/anonymize-proxy.d.ts +5 -2
- package/anonymize-proxy.js +9 -5
- package/browser-pool.d.ts +88 -14
- package/browser-pool.js +151 -28
- package/container-proxy-server.d.ts +0 -1
- package/container-proxy-server.js +0 -1
- package/events.d.ts +0 -1
- package/events.js +0 -1
- package/fingerprinting/hooks.d.ts +0 -1
- package/fingerprinting/hooks.js +24 -9
- package/fingerprinting/types.d.ts +0 -1
- package/fingerprinting/types.js +0 -1
- package/fingerprinting/utils.d.ts +0 -1
- package/fingerprinting/utils.js +0 -1
- package/index.d.ts +14 -7
- package/index.js +5 -3
- package/launch-context.d.ts +19 -3
- package/launch-context.js +12 -4
- package/package.json +7 -8
- package/playwright/playwright-browser.d.ts +0 -1
- package/playwright/playwright-browser.js +0 -1
- package/playwright/playwright-controller.d.ts +0 -1
- package/playwright/playwright-controller.js +20 -10
- package/playwright/playwright-plugin.d.ts +6 -1
- package/playwright/playwright-plugin.js +27 -4
- 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 +16 -9
- package/puppeteer/puppeteer-plugin.d.ts +3 -1
- package/puppeteer/puppeteer-plugin.js +81 -44
- package/remote-browser-pool.d.ts +172 -0
- package/remote-browser-pool.js +191 -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/tsconfig.build.tsbuildinfo +0 -1
- package/utils.d.ts.map +0 -1
- package/utils.js.map +0 -1
package/browser-pool.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AsyncResource } from 'node:async_hooks';
|
|
2
|
+
import { SessionError, serviceLocator } from '@crawlee/core';
|
|
1
3
|
import { FingerprintGenerator } from 'fingerprint-generator';
|
|
2
4
|
import { FingerprintInjector } from 'fingerprint-injector';
|
|
3
5
|
import { nanoid } from 'nanoid';
|
|
@@ -7,7 +9,7 @@ import QuickLRU from 'quick-lru';
|
|
|
7
9
|
import { TypedEmitter } from 'tiny-typed-emitter';
|
|
8
10
|
import { addTimeoutToPromise, tryCancel } from '@apify/timeout';
|
|
9
11
|
import { createFingerprintPreLaunchHook, createPostPageCreateHook, createPrePageCreateHook, } from './fingerprinting/hooks.js';
|
|
10
|
-
|
|
12
|
+
const PAGE_CLOSE_TIMEOUT_MILLIS = 5000;
|
|
11
13
|
const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000;
|
|
12
14
|
const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
13
15
|
/**
|
|
@@ -60,6 +62,7 @@ const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000;
|
|
|
60
62
|
export class BrowserPool extends TypedEmitter {
|
|
61
63
|
browserPlugins;
|
|
62
64
|
maxOpenPagesPerBrowser;
|
|
65
|
+
maxOpenBrowsers;
|
|
63
66
|
retireBrowserAfterPageCount;
|
|
64
67
|
operationTimeoutMillis;
|
|
65
68
|
closeInactiveBrowserAfterMillis;
|
|
@@ -74,6 +77,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
74
77
|
pageCounter = 0;
|
|
75
78
|
pages = new Map();
|
|
76
79
|
pageIds = new WeakMap();
|
|
80
|
+
startingBrowserControllers = new Set();
|
|
77
81
|
activeBrowserControllers = new Set();
|
|
78
82
|
retiredBrowserControllers = new Set();
|
|
79
83
|
pageToBrowserController = new WeakMap();
|
|
@@ -83,8 +87,10 @@ export class BrowserPool extends TypedEmitter {
|
|
|
83
87
|
browserKillerInterval = setInterval(async () => this._closeInactiveRetiredBrowsers(), BROWSER_KILLER_INTERVAL_MILLIS);
|
|
84
88
|
browserRetireInterval;
|
|
85
89
|
limiter = pLimit(1);
|
|
90
|
+
log;
|
|
86
91
|
constructor(options) {
|
|
87
92
|
super();
|
|
93
|
+
this.log = serviceLocator.getLogger().child({ prefix: 'BrowserPool' });
|
|
88
94
|
this.browserKillerInterval.unref();
|
|
89
95
|
ow(options, ow.object.exactShape({
|
|
90
96
|
browserPlugins: ow.array.minLength(1),
|
|
@@ -114,6 +120,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
114
120
|
}
|
|
115
121
|
this.browserPlugins = browserPlugins;
|
|
116
122
|
this.maxOpenPagesPerBrowser = maxOpenPagesPerBrowser;
|
|
123
|
+
this.maxOpenBrowsers = Infinity;
|
|
117
124
|
this.retireBrowserAfterPageCount = retireBrowserAfterPageCount;
|
|
118
125
|
this.operationTimeoutMillis = operationTimeoutSecs * 1000;
|
|
119
126
|
this.closeInactiveBrowserAfterMillis = closeInactiveBrowserAfterSecs * 1000;
|
|
@@ -142,23 +149,46 @@ export class BrowserPool extends TypedEmitter {
|
|
|
142
149
|
* Opens a new page in one of the running browsers or launches
|
|
143
150
|
* a new browser and opens a page there, if no browsers are active,
|
|
144
151
|
* or their page limits have been exceeded.
|
|
152
|
+
*
|
|
153
|
+
* **Session injection (best-effort):** When a {@link NewPageOptions.session|session} is
|
|
154
|
+
* provided, this implementation uses it as a cache key for browser fingerprints (when
|
|
155
|
+
* fingerprinting is enabled) and reads
|
|
156
|
+
* {@link ProxyInfo.url|session.proxyInfo.url} /
|
|
157
|
+
* {@link ProxyInfo.ignoreTlsErrors|session.proxyInfo.ignoreTlsErrors} as defaults
|
|
158
|
+
* for `proxyUrl` and `ignoreTlsErrors` respectively. Explicit `proxyUrl` /
|
|
159
|
+
* `ignoreTlsErrors` values in the options take precedence.
|
|
160
|
+
*
|
|
161
|
+
* Beyond fingerprint caching and proxy configuration, no other session
|
|
162
|
+
* properties are consumed — cookie and header injection remain the
|
|
163
|
+
* crawler's responsibility.
|
|
145
164
|
*/
|
|
146
165
|
async newPage(options = {}) {
|
|
147
|
-
const { id = nanoid(), pageOptions, browserPlugin = this._pickBrowserPlugin(), proxyUrl,
|
|
166
|
+
const { id = nanoid(), pageOptions, browserPlugin = this._pickBrowserPlugin(), session, proxyUrl = session?.proxyInfo?.url, ignoreTlsErrors = session?.proxyInfo?.ignoreTlsErrors, } = options;
|
|
148
167
|
if (this.pages.has(id)) {
|
|
149
168
|
throw new Error(`Page with ID: ${id} already exists.`);
|
|
150
169
|
}
|
|
151
170
|
if (browserPlugin && !this.browserPlugins.includes(browserPlugin)) {
|
|
152
171
|
throw new Error('Provided browserPlugin is not one of the plugins used by BrowserPool.');
|
|
153
172
|
}
|
|
173
|
+
// Bind the limiter callback to the current async-hooks context. p-limit
|
|
174
|
+
// otherwise resumes queued callbacks in the previous task's
|
|
175
|
+
// AsyncLocalStorage context, leaking aborted cancelTasks across unrelated
|
|
176
|
+
// requests (https://github.com/apify/crawlee/issues/3670). Mirrors the
|
|
177
|
+
// fix p-limit landed upstream in v5 (sindresorhus/p-limit#71); v5 is an
|
|
178
|
+
// ESM-only rewrite, so we can't bump it in Crawlee v3.
|
|
179
|
+
// TODO(crawlee@v4): bump p-limit to v5 and drop this AsyncResource.bind wrapper.
|
|
154
180
|
// Limiter is necessary - https://github.com/apify/crawlee/issues/1126
|
|
155
|
-
return this.limiter(async () => {
|
|
156
|
-
let browserController = this._pickBrowserWithFreeCapacity(browserPlugin, {
|
|
181
|
+
return this.limiter(AsyncResource.bind(async () => {
|
|
182
|
+
let browserController = this._pickBrowserWithFreeCapacity(browserPlugin, { proxyUrl });
|
|
157
183
|
if (!browserController)
|
|
158
|
-
browserController = await this._launchBrowser(id, {
|
|
184
|
+
browserController = await this._launchBrowser(id, {
|
|
185
|
+
browserPlugin,
|
|
186
|
+
proxyUrl,
|
|
187
|
+
ignoreTlsErrors,
|
|
188
|
+
});
|
|
159
189
|
tryCancel();
|
|
160
|
-
return await this._createPageForBrowser(id, browserController, pageOptions, proxyUrl);
|
|
161
|
-
});
|
|
190
|
+
return await this._createPageForBrowser(id, browserController, pageOptions, proxyUrl, ignoreTlsErrors);
|
|
191
|
+
}));
|
|
162
192
|
}
|
|
163
193
|
/**
|
|
164
194
|
* Unlike {@link newPage}, `newPageInNewBrowser` always launches a new
|
|
@@ -238,7 +268,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
238
268
|
getPageId(page) {
|
|
239
269
|
return this.pageIds.get(page);
|
|
240
270
|
}
|
|
241
|
-
async _createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl) {
|
|
271
|
+
async _createPageForBrowser(pageId, browserController, pageOptions = {}, proxyUrl, ignoreTlsErrors) {
|
|
242
272
|
// This is needed for concurrent newPage calls to wait for the browser launch.
|
|
243
273
|
// It's not ideal though, we need to come up with a better API.
|
|
244
274
|
// eslint-disable-next-line dot-notation -- accessing private property
|
|
@@ -247,6 +277,12 @@ export class BrowserPool extends TypedEmitter {
|
|
|
247
277
|
const finalPageOptions = browserController.launchContext.useIncognitoPages ? pageOptions : undefined;
|
|
248
278
|
if (finalPageOptions) {
|
|
249
279
|
Object.assign(finalPageOptions, browserController.normalizeProxyOptions(proxyUrl, pageOptions));
|
|
280
|
+
if (ignoreTlsErrors) {
|
|
281
|
+
Object.assign(finalPageOptions, {
|
|
282
|
+
ignoreHTTPSErrors: true,
|
|
283
|
+
acceptInsecureCerts: true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
250
286
|
}
|
|
251
287
|
await this._executeHooks(this.prePageCreateHooks, pageId, browserController, finalPageOptions);
|
|
252
288
|
tryCancel();
|
|
@@ -278,11 +314,14 @@ export class BrowserPool extends TypedEmitter {
|
|
|
278
314
|
*
|
|
279
315
|
*/
|
|
280
316
|
retireBrowserController(browserController) {
|
|
281
|
-
const
|
|
317
|
+
const isStarting = this.startingBrowserControllers.has(browserController);
|
|
318
|
+
const isActive = this.activeBrowserControllers.has(browserController);
|
|
319
|
+
const hasBeenRetiredOrKilled = !isStarting && !isActive;
|
|
282
320
|
if (hasBeenRetiredOrKilled)
|
|
283
321
|
return;
|
|
284
322
|
this.retiredBrowserControllers.add(browserController);
|
|
285
323
|
this.emit("browserRetired" /* BROWSER_POOL_EVENTS.BROWSER_RETIRED */, browserController);
|
|
324
|
+
this.startingBrowserControllers.delete(browserController);
|
|
286
325
|
this.activeBrowserControllers.delete(browserController);
|
|
287
326
|
}
|
|
288
327
|
/**
|
|
@@ -294,12 +333,65 @@ export class BrowserPool extends TypedEmitter {
|
|
|
294
333
|
if (browserController)
|
|
295
334
|
this.retireBrowserController(browserController);
|
|
296
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Releases a page back to the pool. The page is closed and, if the
|
|
338
|
+
* optional `error` is a {@link SessionError}, the browser controller
|
|
339
|
+
* that served the page is retired so that its tainted state (cookies,
|
|
340
|
+
* storage, etc.) cannot leak into future sessions.
|
|
341
|
+
*
|
|
342
|
+
* This is the primary way the crawler should return pages to the pool.
|
|
343
|
+
*
|
|
344
|
+
* @param page The page to release.
|
|
345
|
+
* @param options.error The error that caused the page to be released, if any.
|
|
346
|
+
*/
|
|
347
|
+
async closePage(page, options) {
|
|
348
|
+
if (options?.error instanceof SessionError) {
|
|
349
|
+
this.retireBrowserByPage(page);
|
|
350
|
+
}
|
|
351
|
+
// Puppeteer 25+ can hang `page.close()` indefinitely when the page's navigation was aborted, don't let it block the crawler.
|
|
352
|
+
await addTimeoutToPromise(async () => page.close(), PAGE_CLOSE_TIMEOUT_MILLIS, `page.close() timed out after ${PAGE_CLOSE_TIMEOUT_MILLIS / 1000} seconds`);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Extracts the relevant state (currently just cookies) from a page via its
|
|
356
|
+
* owning {@link BrowserController}. Returns empty state when the page is
|
|
357
|
+
* no longer associated with a controller.
|
|
358
|
+
*
|
|
359
|
+
* As with {@link BrowserPool.injectPageState}, cookies are isolated per
|
|
360
|
+
* page only when the pool is configured with `useIncognitoPages: true`.
|
|
361
|
+
* With the default `useIncognitoPages: false`, the extracted cookies
|
|
362
|
+
* include those set by any sibling page sharing the same browser.
|
|
363
|
+
*/
|
|
364
|
+
async extractPageState(page) {
|
|
365
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
366
|
+
if (!controller) {
|
|
367
|
+
return { cookies: [] };
|
|
368
|
+
}
|
|
369
|
+
return { cookies: await controller.getCookies(page) };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Injects state into a page via its owning {@link BrowserController}.
|
|
373
|
+
*
|
|
374
|
+
* No-op when the page is no longer associated with a controller.
|
|
375
|
+
*
|
|
376
|
+
* Note that cookies are isolated per page only when the pool is configured
|
|
377
|
+
* with `useIncognitoPages: true` — each page then gets its own browser
|
|
378
|
+
* context. With the default `useIncognitoPages: false`, all pages in a
|
|
379
|
+
* browser share a single context, so injected cookies are visible to every
|
|
380
|
+
* page served by that browser.
|
|
381
|
+
*/
|
|
382
|
+
async injectPageState(page, state) {
|
|
383
|
+
const controller = this.getBrowserControllerByPage(page);
|
|
384
|
+
if (!controller) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
await controller.setCookies(page, state.cookies);
|
|
388
|
+
}
|
|
297
389
|
/**
|
|
298
390
|
* Removes all active browsers from the pool. The browsers will be
|
|
299
391
|
* closed after all their pages are closed.
|
|
300
392
|
*/
|
|
301
393
|
retireAllBrowsers() {
|
|
302
|
-
this.activeBrowserControllers.forEach((controller) => {
|
|
394
|
+
[...this.startingBrowserControllers, ...this.activeBrowserControllers].forEach((controller) => {
|
|
303
395
|
this.retireBrowserController(controller);
|
|
304
396
|
});
|
|
305
397
|
}
|
|
@@ -326,23 +418,36 @@ export class BrowserPool extends TypedEmitter {
|
|
|
326
418
|
this._teardown();
|
|
327
419
|
}
|
|
328
420
|
_teardown() {
|
|
421
|
+
this.startingBrowserControllers.clear();
|
|
329
422
|
this.activeBrowserControllers.clear();
|
|
330
423
|
this.retiredBrowserControllers.clear();
|
|
331
424
|
this.removeAllListeners();
|
|
332
425
|
}
|
|
333
426
|
_getAllBrowserControllers() {
|
|
334
|
-
return new Set([
|
|
427
|
+
return new Set([
|
|
428
|
+
...this.startingBrowserControllers,
|
|
429
|
+
...this.activeBrowserControllers,
|
|
430
|
+
...this.retiredBrowserControllers,
|
|
431
|
+
]);
|
|
335
432
|
}
|
|
336
433
|
async _launchBrowser(pageId, options) {
|
|
337
|
-
const { browserPlugin, launchOptions,
|
|
434
|
+
const { browserPlugin, launchOptions, proxyUrl, ignoreTlsErrors } = options;
|
|
338
435
|
const browserController = browserPlugin.createController();
|
|
339
|
-
this.
|
|
436
|
+
this.startingBrowserControllers.add(browserController);
|
|
340
437
|
const launchContext = browserPlugin.createLaunchContext({
|
|
341
438
|
id: pageId,
|
|
342
439
|
launchOptions,
|
|
343
|
-
proxyTier,
|
|
344
440
|
proxyUrl,
|
|
345
441
|
});
|
|
442
|
+
// Disable SSL verification for MITM proxies
|
|
443
|
+
if (ignoreTlsErrors) {
|
|
444
|
+
/**
|
|
445
|
+
* @see https://playwright.dev/docs/api/class-browser/#browser-new-context
|
|
446
|
+
* @see https://github.com/puppeteer/puppeteer/blob/main/docs/api.md
|
|
447
|
+
*/
|
|
448
|
+
launchContext.launchOptions.ignoreHTTPSErrors = true;
|
|
449
|
+
launchContext.launchOptions.acceptInsecureCerts = true;
|
|
450
|
+
}
|
|
346
451
|
try {
|
|
347
452
|
// If the hooks or the launch fails, we need to delete the controller,
|
|
348
453
|
// because otherwise it would be stuck in limbo without a browser.
|
|
@@ -353,11 +458,10 @@ export class BrowserPool extends TypedEmitter {
|
|
|
353
458
|
browserController.assignBrowser(browser, launchContext);
|
|
354
459
|
}
|
|
355
460
|
catch (err) {
|
|
356
|
-
this.
|
|
461
|
+
this.startingBrowserControllers.delete(browserController);
|
|
357
462
|
throw err;
|
|
358
463
|
}
|
|
359
|
-
log.debug('Launched new browser.', { id: browserController.id });
|
|
360
|
-
browserController.proxyTier = proxyTier;
|
|
464
|
+
this.log.debug('Launched new browser.', { id: browserController.id });
|
|
361
465
|
browserController.proxyUrl = proxyUrl;
|
|
362
466
|
try {
|
|
363
467
|
// If the launch fails on the post-launch hooks, we need to clean up
|
|
@@ -365,9 +469,9 @@ export class BrowserPool extends TypedEmitter {
|
|
|
365
469
|
await this._executeHooks(this.postLaunchHooks, pageId, browserController);
|
|
366
470
|
}
|
|
367
471
|
catch (err) {
|
|
368
|
-
this.
|
|
472
|
+
this.startingBrowserControllers.delete(browserController);
|
|
369
473
|
browserController.close().catch((closeErr) => {
|
|
370
|
-
log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
474
|
+
this.log.error(`Could not close browser whose post-launch hooks failed.\nCause:${closeErr.message}`, {
|
|
371
475
|
id: browserController.id,
|
|
372
476
|
});
|
|
373
477
|
});
|
|
@@ -375,6 +479,8 @@ export class BrowserPool extends TypedEmitter {
|
|
|
375
479
|
}
|
|
376
480
|
tryCancel();
|
|
377
481
|
browserController.activate();
|
|
482
|
+
this.startingBrowserControllers.delete(browserController);
|
|
483
|
+
this.activeBrowserControllers.add(browserController);
|
|
378
484
|
this.emit("browserLaunched" /* BROWSER_POOL_EVENTS.BROWSER_LAUNCHED */, browserController);
|
|
379
485
|
return browserController;
|
|
380
486
|
}
|
|
@@ -392,13 +498,11 @@ export class BrowserPool extends TypedEmitter {
|
|
|
392
498
|
const hasCapacity = controller.activePages < this.maxOpenPagesPerBrowser;
|
|
393
499
|
const isCorrectPlugin = controller.browserPlugin === browserPlugin;
|
|
394
500
|
const isSameProxyUrl = controller.proxyUrl === options?.proxyUrl;
|
|
395
|
-
const isCorrectProxyTier = controller.proxyTier === options?.proxyTier;
|
|
396
501
|
return (isCorrectPlugin &&
|
|
397
502
|
hasCapacity &&
|
|
398
|
-
(
|
|
399
|
-
(options?.proxyTier && isCorrectProxyTier) ||
|
|
503
|
+
(!controller.launchContext.browserPerProxy ||
|
|
400
504
|
(options?.proxyUrl && isSameProxyUrl) ||
|
|
401
|
-
(!options?.proxyUrl && !
|
|
505
|
+
(!options?.proxyUrl && !controller.proxyUrl)));
|
|
402
506
|
});
|
|
403
507
|
}
|
|
404
508
|
async _closeInactiveRetiredBrowsers() {
|
|
@@ -409,14 +513,14 @@ export class BrowserPool extends TypedEmitter {
|
|
|
409
513
|
const isBrowserEmpty = controller.activePages === 0;
|
|
410
514
|
if (isBrowserIdle || isBrowserEmpty) {
|
|
411
515
|
const { id } = controller;
|
|
412
|
-
log.debug('Closing retired browser.', { id });
|
|
516
|
+
this.log.debug('Closing retired browser.', { id });
|
|
413
517
|
await controller.close();
|
|
414
518
|
this.retiredBrowserControllers.delete(controller);
|
|
415
519
|
closedBrowserIds.push(id);
|
|
416
520
|
}
|
|
417
521
|
}
|
|
418
522
|
if (closedBrowserIds.length) {
|
|
419
|
-
log.debug('Closed retired browsers.', {
|
|
523
|
+
this.log.debug('Closed retired browsers.', {
|
|
420
524
|
count: closedBrowserIds.length,
|
|
421
525
|
closedBrowserIds,
|
|
422
526
|
});
|
|
@@ -429,7 +533,7 @@ export class BrowserPool extends TypedEmitter {
|
|
|
429
533
|
page.close = async (...args) => {
|
|
430
534
|
await this._executeHooks(this.prePageCloseHooks, page, browserController);
|
|
431
535
|
await originalPageClose.apply(page, args).catch((err) => {
|
|
432
|
-
log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
|
|
536
|
+
this.log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id });
|
|
433
537
|
});
|
|
434
538
|
await this._executeHooks(this.postPageCloseHooks, pageId, browserController);
|
|
435
539
|
this.pages.delete(pageId);
|
|
@@ -447,13 +551,33 @@ export class BrowserPool extends TypedEmitter {
|
|
|
447
551
|
// Run this with a delay, otherwise page.close()
|
|
448
552
|
// might fail with "Protocol error (Target.closeTarget): Target closed."
|
|
449
553
|
setTimeout(() => {
|
|
450
|
-
log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
554
|
+
this.log.debug('Closing retired browser because it has no active pages', { id: browserController.id });
|
|
451
555
|
void browserController.close().finally(() => {
|
|
452
556
|
this.retiredBrowserControllers.delete(browserController);
|
|
453
557
|
});
|
|
454
558
|
}, PAGE_CLOSE_KILL_TIMEOUT_MILLIS);
|
|
455
559
|
}
|
|
456
560
|
}
|
|
561
|
+
/**
|
|
562
|
+
* Returns `true` if the pool can accept a new browser launch without exceeding
|
|
563
|
+
* {@link BrowserPoolOptions.maxOpenBrowsers}. Counts starting, active, and retired browsers.
|
|
564
|
+
*/
|
|
565
|
+
hasFreeBrowserSlot() {
|
|
566
|
+
const total = this.startingBrowserControllers.size +
|
|
567
|
+
this.activeBrowserControllers.size +
|
|
568
|
+
this.retiredBrowserControllers.size;
|
|
569
|
+
return total < this.maxOpenBrowsers;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Returns `true` if any active browser has room for another page.
|
|
573
|
+
*/
|
|
574
|
+
hasActiveBrowserWithFreeCapacity() {
|
|
575
|
+
for (const controller of this.activeBrowserControllers) {
|
|
576
|
+
if (controller.activePages < this.maxOpenPagesPerBrowser)
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
457
581
|
_initializeFingerprinting() {
|
|
458
582
|
const { useFingerprintCache = true, fingerprintCacheSize = 10_000 } = this.fingerprintOptions;
|
|
459
583
|
this.fingerprintGenerator = new FingerprintGenerator(this.fingerprintOptions.fingerprintGeneratorOptions);
|
|
@@ -474,4 +598,3 @@ export class BrowserPool extends TypedEmitter {
|
|
|
474
598
|
this.postPageCreateHooks = [createPostPageCreateHook(this.fingerprintInjector), ...this.postPageCreateHooks];
|
|
475
599
|
}
|
|
476
600
|
}
|
|
477
|
-
//# sourceMappingURL=browser-pool.js.map
|
package/events.d.ts
CHANGED
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
|
package/fingerprinting/hooks.js
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
import { PlaywrightPlugin } from '../playwright/playwright-plugin.js';
|
|
2
2
|
import { PuppeteerPlugin } from '../puppeteer/puppeteer-plugin.js';
|
|
3
3
|
import { getGeneratorDefaultOptions } from './utils.js';
|
|
4
|
+
function applySessionHints(base, fingerprint) {
|
|
5
|
+
if (!fingerprint)
|
|
6
|
+
return base;
|
|
7
|
+
return {
|
|
8
|
+
...base,
|
|
9
|
+
...(fingerprint.browser ? { browsers: [{ name: fingerprint.browser }] } : {}),
|
|
10
|
+
...(fingerprint.platform ? { operatingSystems: [fingerprint.platform] } : {}),
|
|
11
|
+
...(fingerprint.device ? { devices: [fingerprint.device] } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
15
|
* @internal
|
|
6
16
|
*/
|
|
7
17
|
export function createFingerprintPreLaunchHook(browserPool) {
|
|
8
18
|
const { fingerprintGenerator, fingerprintCache, fingerprintOptions: { fingerprintGeneratorOptions }, } = browserPool;
|
|
9
19
|
return (_pageId, launchContext) => {
|
|
20
|
+
// Remote browsers may have their own fingerprinting — skip local fingerprint injection
|
|
21
|
+
if (launchContext.isRemote)
|
|
22
|
+
return;
|
|
10
23
|
const { useIncognitoPages } = launchContext;
|
|
11
|
-
const
|
|
24
|
+
const session = launchContext.session;
|
|
25
|
+
const cacheKey = session?.id ?? launchContext.proxyUrl;
|
|
12
26
|
const { launchOptions } = launchContext;
|
|
13
|
-
// If no options are passed we try to pass best default options as possible to match browser and OS.
|
|
14
|
-
const fingerprintGeneratorFinalOptions = fingerprintGeneratorOptions || getGeneratorDefaultOptions(launchContext);
|
|
15
27
|
let fingerprint;
|
|
16
28
|
if (cacheKey && fingerprintCache?.has(cacheKey)) {
|
|
17
29
|
fingerprint = fingerprintCache.get(cacheKey);
|
|
18
30
|
}
|
|
19
|
-
else if (cacheKey) {
|
|
20
|
-
fingerprint = fingerprintGenerator.getFingerprint(fingerprintGeneratorFinalOptions);
|
|
21
|
-
fingerprintCache?.set(cacheKey, fingerprint);
|
|
22
|
-
}
|
|
23
31
|
else {
|
|
24
|
-
|
|
32
|
+
const baseOptions = fingerprintGeneratorOptions || getGeneratorDefaultOptions(launchContext);
|
|
33
|
+
const finalOptions = applySessionHints(baseOptions, session?.fingerprint);
|
|
34
|
+
fingerprint = fingerprintGenerator.getFingerprint(finalOptions);
|
|
35
|
+
if (cacheKey)
|
|
36
|
+
fingerprintCache?.set(cacheKey, fingerprint);
|
|
25
37
|
}
|
|
26
38
|
launchContext.extend({ fingerprint });
|
|
27
39
|
if (useIncognitoPages) {
|
|
@@ -41,6 +53,8 @@ export function createFingerprintPreLaunchHook(browserPool) {
|
|
|
41
53
|
export function createPrePageCreateHook() {
|
|
42
54
|
return (_pageId, browserController, pageOptions) => {
|
|
43
55
|
const { launchContext, browserPlugin } = browserController;
|
|
56
|
+
if (launchContext.isRemote)
|
|
57
|
+
return;
|
|
44
58
|
const { fingerprint } = launchContext.fingerprint;
|
|
45
59
|
if (launchContext.useIncognitoPages && browserPlugin instanceof PlaywrightPlugin && pageOptions) {
|
|
46
60
|
pageOptions.userAgent ??= fingerprint.navigator.userAgent;
|
|
@@ -57,6 +71,8 @@ export function createPrePageCreateHook() {
|
|
|
57
71
|
export function createPostPageCreateHook(fingerprintInjector) {
|
|
58
72
|
return async (page, browserController) => {
|
|
59
73
|
const { browserPlugin, launchContext } = browserController;
|
|
74
|
+
if (launchContext.isRemote)
|
|
75
|
+
return;
|
|
60
76
|
const fingerprint = launchContext.fingerprint;
|
|
61
77
|
// TODO this will require refactoring, we should use common API instead of branching based on plugin type,
|
|
62
78
|
// and there should be no public methods specific to some browser.
|
|
@@ -79,4 +95,3 @@ export function createPostPageCreateHook(fingerprintInjector) {
|
|
|
79
95
|
}
|
|
80
96
|
};
|
|
81
97
|
}
|
|
82
|
-
//# sourceMappingURL=hooks.js.map
|
package/fingerprinting/types.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import type { LaunchContext } from '../launch-context.js';
|
|
2
2
|
import type { FingerprintGeneratorOptions } from './types.js';
|
|
3
3
|
export declare const getGeneratorDefaultOptions: (launchContext: LaunchContext) => FingerprintGeneratorOptions;
|
|
4
|
-
//# sourceMappingURL=utils.d.ts.map
|
package/fingerprinting/utils.js
CHANGED
package/index.d.ts
CHANGED
|
@@ -28,13 +28,20 @@ export * from './browser-pool.js';
|
|
|
28
28
|
export * from './playwright/playwright-plugin.js';
|
|
29
29
|
export * from './puppeteer/puppeteer-plugin.js';
|
|
30
30
|
export * from './events.js';
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
31
|
+
export type { BrowserSpecification, FingerprintGenerator, FingerprintGeneratorOptions, GetFingerprintReturn, } from './fingerprinting/types.js';
|
|
32
|
+
export { BrowserName, DeviceCategory, OperatingSystemsName } from './fingerprinting/types.js';
|
|
33
|
+
export type { BrowserControllerEvents, IBrowserController, IBrowserLaunchContext, } from './abstract-classes/browser-controller.js';
|
|
34
|
+
export { BrowserController } from './abstract-classes/browser-controller.js';
|
|
33
35
|
export { PuppeteerController } from './puppeteer/puppeteer-controller.js';
|
|
34
36
|
export { PlaywrightController } from './playwright/playwright-controller.js';
|
|
35
37
|
export { PlaywrightBrowser } from './playwright/playwright-browser.js';
|
|
36
|
-
export { CommonPage, CommonLibrary,
|
|
37
|
-
export {
|
|
38
|
-
export {
|
|
39
|
-
export {
|
|
40
|
-
|
|
38
|
+
export type { CommonPage, CommonLibrary, BrowserPluginOptions, CreateLaunchContextOptions, } from './abstract-classes/browser-plugin.js';
|
|
39
|
+
export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT } from './abstract-classes/browser-plugin.js';
|
|
40
|
+
export type { LaunchContextOptions } from './launch-context.js';
|
|
41
|
+
export { LaunchContext } from './launch-context.js';
|
|
42
|
+
export { RemoteBrowserProvider } from './remote-browser-provider.js';
|
|
43
|
+
export { RemoteBrowserPool } from './remote-browser-pool.js';
|
|
44
|
+
export type { RemoteBrowserPoolOptions, CrawlerRemoteBrowserOptions, RemoteBrowserEndpoint, ResolvedRemoteEndpoint, RemoteConnection, RemoteConnectionParameters, } from './remote-browser-pool.js';
|
|
45
|
+
export type { InferBrowserPluginArray, UnwrapPromise } from './utils.js';
|
|
46
|
+
export { anonymizeProxySugar, type AnonymizeProxySugarOptions } from './anonymize-proxy.js';
|
|
47
|
+
export type { IBrowserPool, NewPageOptions } from '@crawlee/types';
|
package/index.js
CHANGED
|
@@ -26,11 +26,13 @@ export * from './browser-pool.js';
|
|
|
26
26
|
export * from './playwright/playwright-plugin.js';
|
|
27
27
|
export * from './puppeteer/puppeteer-plugin.js';
|
|
28
28
|
export * from './events.js';
|
|
29
|
-
export { BrowserName, DeviceCategory, OperatingSystemsName
|
|
29
|
+
export { BrowserName, DeviceCategory, OperatingSystemsName } from './fingerprinting/types.js';
|
|
30
30
|
export { BrowserController } from './abstract-classes/browser-controller.js';
|
|
31
31
|
export { PuppeteerController } from './puppeteer/puppeteer-controller.js';
|
|
32
32
|
export { PlaywrightController } from './playwright/playwright-controller.js';
|
|
33
33
|
export { PlaywrightBrowser } from './playwright/playwright-browser.js';
|
|
34
|
-
export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT
|
|
34
|
+
export { BrowserPlugin, BrowserLaunchError, DEFAULT_USER_AGENT } from './abstract-classes/browser-plugin.js';
|
|
35
35
|
export { LaunchContext } from './launch-context.js';
|
|
36
|
-
|
|
36
|
+
export { RemoteBrowserProvider } from './remote-browser-provider.js';
|
|
37
|
+
export { RemoteBrowserPool } from './remote-browser-pool.js';
|
|
38
|
+
export { anonymizeProxySugar } from './anonymize-proxy.js';
|
package/launch-context.d.ts
CHANGED
|
@@ -43,7 +43,17 @@ export interface LaunchContextOptions<Library extends CommonLibrary = CommonLibr
|
|
|
43
43
|
*/
|
|
44
44
|
userDataDir?: string;
|
|
45
45
|
proxyUrl?: string;
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* If set to `true`, TLS certificate errors from the upstream proxy will be ignored.
|
|
48
|
+
* This is useful when using HTTPS proxies with self-signed certificates.
|
|
49
|
+
*/
|
|
50
|
+
ignoreProxyCertificate?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether this launch context represents a connection to a remote browser
|
|
53
|
+
* rather than a locally launched one.
|
|
54
|
+
* @default false
|
|
55
|
+
*/
|
|
56
|
+
isRemote?: boolean;
|
|
47
57
|
}
|
|
48
58
|
export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary, LibraryOptions extends Dictionary | undefined = Parameters<Library['launch']>[0], LaunchResult extends CommonBrowser = UnwrapPromise<ReturnType<Library['launch']>>, NewPageOptions = Parameters<LaunchResult['newPage']>[0], NewPageResult = UnwrapPromise<ReturnType<LaunchResult['newPage']>>> {
|
|
49
59
|
id?: string;
|
|
@@ -52,10 +62,17 @@ export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary
|
|
|
52
62
|
useIncognitoPages: boolean;
|
|
53
63
|
browserPerProxy?: boolean;
|
|
54
64
|
userDataDir: string;
|
|
55
|
-
|
|
65
|
+
readonly isRemote: boolean;
|
|
66
|
+
ignoreProxyCertificate?: boolean;
|
|
56
67
|
private _proxyUrl?;
|
|
57
68
|
private readonly _reservedFieldNames;
|
|
58
69
|
fingerprint?: BrowserFingerprintWithHeaders;
|
|
70
|
+
/**
|
|
71
|
+
* Token identifying the remote browser session this context connected to, set by the plugin and read by
|
|
72
|
+
* the {@link RemoteBrowserPool} to release the session on close. Only present for remote connections.
|
|
73
|
+
* @internal
|
|
74
|
+
*/
|
|
75
|
+
_remoteToken?: number;
|
|
59
76
|
[K: PropertyKey]: unknown;
|
|
60
77
|
constructor(options: LaunchContextOptions<Library, LibraryOptions, LaunchResult, NewPageOptions, NewPageResult>);
|
|
61
78
|
/**
|
|
@@ -76,4 +93,3 @@ export declare class LaunchContext<Library extends CommonLibrary = CommonLibrary
|
|
|
76
93
|
*/
|
|
77
94
|
get proxyUrl(): string | undefined;
|
|
78
95
|
}
|
|
79
|
-
//# sourceMappingURL=launch-context.d.ts.map
|
package/launch-context.js
CHANGED
|
@@ -5,19 +5,27 @@ export class LaunchContext {
|
|
|
5
5
|
useIncognitoPages;
|
|
6
6
|
browserPerProxy;
|
|
7
7
|
userDataDir;
|
|
8
|
-
|
|
8
|
+
isRemote;
|
|
9
|
+
ignoreProxyCertificate;
|
|
9
10
|
_proxyUrl;
|
|
10
11
|
_reservedFieldNames = [...Reflect.ownKeys(this), 'extend'];
|
|
11
12
|
fingerprint;
|
|
13
|
+
/**
|
|
14
|
+
* Token identifying the remote browser session this context connected to, set by the plugin and read by
|
|
15
|
+
* the {@link RemoteBrowserPool} to release the session on close. Only present for remote connections.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
_remoteToken;
|
|
12
19
|
constructor(options) {
|
|
13
|
-
const { id, browserPlugin, launchOptions, proxyUrl, useIncognitoPages, browserPerProxy, userDataDir = '',
|
|
20
|
+
const { id, browserPlugin, launchOptions, proxyUrl, useIncognitoPages, browserPerProxy, userDataDir = '', ignoreProxyCertificate, isRemote, } = options;
|
|
14
21
|
this.id = id;
|
|
15
22
|
this.browserPlugin = browserPlugin;
|
|
16
23
|
this.launchOptions = launchOptions;
|
|
17
24
|
this.browserPerProxy = browserPerProxy ?? false;
|
|
18
25
|
this.useIncognitoPages = useIncognitoPages ?? false;
|
|
19
26
|
this.userDataDir = userDataDir;
|
|
20
|
-
this.
|
|
27
|
+
this.ignoreProxyCertificate = ignoreProxyCertificate ?? false;
|
|
28
|
+
this.isRemote = isRemote ?? false;
|
|
21
29
|
this._proxyUrl = proxyUrl;
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
@@ -43,6 +51,7 @@ export class LaunchContext {
|
|
|
43
51
|
*/
|
|
44
52
|
set proxyUrl(url) {
|
|
45
53
|
if (!url) {
|
|
54
|
+
this._proxyUrl = undefined;
|
|
46
55
|
return;
|
|
47
56
|
}
|
|
48
57
|
const urlInstance = new URL(url);
|
|
@@ -59,4 +68,3 @@ export class LaunchContext {
|
|
|
59
68
|
return this._proxyUrl;
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
|
-
//# sourceMappingURL=launch-context.js.map
|