@govtechsg/oobee 0.10.98 → 0.11.0
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/AGENTS.md +25 -0
- package/CRAWL.md +260 -0
- package/README.md +4 -0
- package/dist/combine.js +7 -0
- package/dist/constants/common.js +7 -0
- package/dist/constants/constants.js +17 -0
- package/dist/crawlers/commonCrawlerFunc.js +89 -48
- package/dist/crawlers/crawlDomain.js +88 -30
- package/dist/crawlers/crawlSitemap.js +123 -18
- package/dist/mergeAxeResults.js +2 -1
- package/dist/utils.js +36 -9
- package/oobee-client-scanner.js +2 -2
- package/package.json +1 -1
- package/src/combine.ts +7 -0
- package/src/constants/common.ts +9 -0
- package/src/constants/constants.ts +17 -0
- package/src/crawlers/commonCrawlerFunc.ts +101 -62
- package/src/crawlers/crawlDomain.ts +89 -30
- package/src/crawlers/crawlSitemap.ts +128 -19
- package/src/mergeAxeResults.ts +2 -1
- package/src/utils.ts +31 -8
- /package/{0a93900f-3345-4f25-8be7-c2d3748034be.txt → f0ce5c33-4dd0-4e14-8e67-1394f57ff517.txt} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crawlee from 'crawlee';
|
|
2
2
|
import { CrawlRateController } from './crawlRateController.js';
|
|
3
|
-
import { createCrawleeSubFolders, getPreLaunchHook, preNavigationHooks, runAxeScript, isUrlPdf, shouldSkipClickDueToDisallowedHref, shouldSkipDueToUnsupportedContent, splitAuthHeaders, } from './commonCrawlerFunc.js';
|
|
3
|
+
import { createCrawleeSubFolders, getPreLaunchHook, getPostPageCloseHook, preNavigationHooks, runAxeScript, isUrlPdf, shouldSkipClickDueToDisallowedHref, shouldSkipDueToUnsupportedContent, splitAuthHeaders, } from './commonCrawlerFunc.js';
|
|
4
4
|
import constants, { blackListedFileExtensions, guiInfoStatusTypes, cssQuerySelectors, STATUS_CODE_METADATA, disallowedListOfPatterns, disallowedSelectorPatterns, FileTypes, } from '../constants/constants.js';
|
|
5
5
|
import { getPlaywrightLaunchOptions, isBlacklistedFileExtensions, isSkippedUrl, isDisallowedInRobotsTxt, getUrlsFromRobotsTxt, waitForPageLoaded, } from '../constants/common.js';
|
|
6
6
|
import { areLinksEqual, isFollowStrategy, isSameHostname, normUrl, register } from '../utils.js';
|
|
@@ -304,12 +304,24 @@ const crawlDomain = async ({ url, randomToken, host: _host, viewportSettings, ma
|
|
|
304
304
|
};
|
|
305
305
|
},
|
|
306
306
|
],
|
|
307
|
+
postPageCloseHooks: [getPostPageCloseHook(userDataDirectory)],
|
|
307
308
|
},
|
|
308
309
|
requestQueue,
|
|
309
310
|
maxRequestRetries: 3,
|
|
310
311
|
maxSessionRotations: 1,
|
|
311
312
|
preNavigationHooks: [
|
|
312
313
|
...preNavigationHooks(extraHTTPHeaders),
|
|
314
|
+
async ({ request }) => {
|
|
315
|
+
const url = request.url.toLowerCase();
|
|
316
|
+
try {
|
|
317
|
+
const pathname = new URL(url).pathname;
|
|
318
|
+
const ext = pathname.split('.').pop();
|
|
319
|
+
if (ext && blackListedFileExtensions.includes(ext)) {
|
|
320
|
+
request.skipNavigation = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch { }
|
|
324
|
+
},
|
|
313
325
|
],
|
|
314
326
|
postNavigationHooks: [
|
|
315
327
|
async (crawlingContext) => {
|
|
@@ -415,20 +427,17 @@ const crawlDomain = async ({ url, randomToken, host: _host, viewportSettings, ma
|
|
|
415
427
|
if (shouldSkipDueToUnsupportedContent(response, request.url) ||
|
|
416
428
|
(request.skipNavigation && actualUrl === 'about:blank')) {
|
|
417
429
|
if (!isScanPdfs) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
httpStatusCode: 0,
|
|
430
|
-
});
|
|
431
|
-
*/
|
|
430
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
431
|
+
numScanned: urlsCrawled.scanned.length,
|
|
432
|
+
urlScanned: request.url,
|
|
433
|
+
});
|
|
434
|
+
urlsCrawled.userExcluded.push({
|
|
435
|
+
url: request.url,
|
|
436
|
+
pageTitle: request.url,
|
|
437
|
+
actualUrl: request.url,
|
|
438
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
439
|
+
httpStatusCode: 1,
|
|
440
|
+
});
|
|
432
441
|
return;
|
|
433
442
|
}
|
|
434
443
|
const { pdfFileName, url: downloadedPdfUrl } = handlePdfDownload(randomToken, pdfDownloads, request, sendRequest, urlsCrawled);
|
|
@@ -436,20 +445,17 @@ const crawlDomain = async ({ url, randomToken, host: _host, viewportSettings, ma
|
|
|
436
445
|
return;
|
|
437
446
|
}
|
|
438
447
|
if (isBlacklistedFileExtensions(actualUrl, blackListedFileExtensions)) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
httpStatusCode: 0,
|
|
451
|
-
});
|
|
452
|
-
*/
|
|
448
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
449
|
+
numScanned: urlsCrawled.scanned.length,
|
|
450
|
+
urlScanned: request.url,
|
|
451
|
+
});
|
|
452
|
+
urlsCrawled.userExcluded.push({
|
|
453
|
+
url: request.url,
|
|
454
|
+
pageTitle: request.url,
|
|
455
|
+
actualUrl,
|
|
456
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
457
|
+
httpStatusCode: 1,
|
|
458
|
+
});
|
|
453
459
|
return;
|
|
454
460
|
}
|
|
455
461
|
if (!isFollowStrategy(url, actualUrl, strategy) &&
|
|
@@ -634,7 +640,57 @@ const crawlDomain = async ({ url, randomToken, host: _host, viewportSettings, ma
|
|
|
634
640
|
if (isAbortingScanNow) {
|
|
635
641
|
return;
|
|
636
642
|
}
|
|
643
|
+
// Handle download-triggered navigation errors: Playwright throws
|
|
644
|
+
// "Download is starting" when page.goto() hits a file download URL.
|
|
645
|
+
// Crawlee retries 3 times (all fail) then lands here.
|
|
646
|
+
const isDownloadError = request.errorMessages?.some((msg) => msg.includes('Download is starting'));
|
|
647
|
+
if (isDownloadError) {
|
|
648
|
+
if (isScanPdfs) {
|
|
649
|
+
// Re-enqueue with skipNavigation so the requestHandler's PDF download path handles it
|
|
650
|
+
try {
|
|
651
|
+
await requestQueue.addRequest({
|
|
652
|
+
url: request.url,
|
|
653
|
+
skipNavigation: true,
|
|
654
|
+
label: request.url,
|
|
655
|
+
uniqueKey: `download_${request.url}`,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch { }
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
662
|
+
numScanned: urlsCrawled.scanned.length,
|
|
663
|
+
urlScanned: request.url,
|
|
664
|
+
});
|
|
665
|
+
urlsCrawled.userExcluded.push({
|
|
666
|
+
url: request.url,
|
|
667
|
+
pageTitle: request.url,
|
|
668
|
+
actualUrl: request.url,
|
|
669
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
670
|
+
httpStatusCode: 1,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
637
675
|
const status = response?.status();
|
|
676
|
+
// Re-enqueue rate-limited (403) URLs once for a retry after concurrency recovers.
|
|
677
|
+
// Without this, URLs that fail during a rate-limit burst are permanently lost
|
|
678
|
+
// even though the site is accessible at lower concurrency.
|
|
679
|
+
// Don't call onFailure here — the re-enqueued request will get a fresh attempt.
|
|
680
|
+
// If it fails again (rateLimitRetried=true), it falls through to the normal
|
|
681
|
+
// onFailure + circuit breaker path below.
|
|
682
|
+
if (status === 403 && !request.userData?.rateLimitRetried) {
|
|
683
|
+
try {
|
|
684
|
+
await requestQueue.addRequest({
|
|
685
|
+
url: request.url,
|
|
686
|
+
label: request.url,
|
|
687
|
+
uniqueKey: `ratelimit_${request.url}`,
|
|
688
|
+
userData: { rateLimitRetried: true },
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch { }
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
638
694
|
if (rateController.onFailure(status, crawler.autoscaledPool)) {
|
|
639
695
|
consoleLogger.info(`Aborting crawl: consecutive HTTP failures threshold reached (site may be rate-limiting). Successfully scanned ${urlsCrawled.scanned.length} pages.`);
|
|
640
696
|
isAbortingScanNow = true;
|
|
@@ -669,7 +725,9 @@ const crawlDomain = async ({ url, randomToken, host: _host, viewportSettings, ma
|
|
|
669
725
|
await crawler.run();
|
|
670
726
|
// Additional passes: keep re-visiting scanned seed-hostname pages for
|
|
671
727
|
// click-discovery until no new pages are found or limits are reached.
|
|
672
|
-
|
|
728
|
+
// Skip when called from intelligent sitemap — the domain phase is only meant
|
|
729
|
+
// to discover new pages via <a> links, not re-click 3000+ already-scanned pages.
|
|
730
|
+
if (!safeMode && !isAbortingScanNow && !durationExceeded && !fromCrawlIntelligentSitemap) {
|
|
673
731
|
const seedHostname = new URL(url).hostname;
|
|
674
732
|
const clickPassVisited = new Set();
|
|
675
733
|
let prevScannedCount;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import crawlee, { EnqueueStrategy, RequestList } from 'crawlee';
|
|
2
2
|
import { CrawlRateController } from './crawlRateController.js';
|
|
3
|
-
import { createCrawleeSubFolders, getPreLaunchHook, preNavigationHooks, runAxeScript, splitAuthHeaders, } from './commonCrawlerFunc.js';
|
|
4
|
-
import constants, { STATUS_CODE_METADATA, guiInfoStatusTypes, disallowedListOfPatterns, FileTypes, } from '../constants/constants.js';
|
|
5
|
-
import { getLinksFromSitemap, getPlaywrightLaunchOptions, isSkippedUrl, waitForPageLoaded, isFilePath, } from '../constants/common.js';
|
|
3
|
+
import { createCrawleeSubFolders, getPreLaunchHook, getPostPageCloseHook, preNavigationHooks, runAxeScript, isUrlPdf, splitAuthHeaders, } from './commonCrawlerFunc.js';
|
|
4
|
+
import constants, { STATUS_CODE_METADATA, guiInfoStatusTypes, blackListedFileExtensions, disallowedListOfPatterns, disallowedSelectorPatterns, FileTypes, } from '../constants/constants.js';
|
|
5
|
+
import { getLinksFromSitemap, getPlaywrightLaunchOptions, isDisallowedInRobotsTxt, isSkippedUrl, waitForPageLoaded, isFilePath, } from '../constants/common.js';
|
|
6
6
|
import { areLinksEqual, isFollowStrategy, isWhitelistedContentType, normUrl, register } from '../utils.js';
|
|
7
7
|
import { handlePdfDownload, runPdfScan, mapPdfScanResults, doPdfScreenshots, } from './pdfScanFunc.js';
|
|
8
8
|
import { consoleLogger, guiInfoLog } from '../logs.js';
|
|
@@ -41,6 +41,11 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
41
41
|
const requestList = await RequestList.open({
|
|
42
42
|
sources: linksFromSitemap,
|
|
43
43
|
});
|
|
44
|
+
// Always create a request queue alongside the request list. An empty queue
|
|
45
|
+
// has zero impact on crawl behavior (Crawlee processes RequestList first).
|
|
46
|
+
// Having it available enables: download re-enqueue for PDF scanning,
|
|
47
|
+
// 403 rate-limit retry, and enqueueLinks for intelligent sitemap discovery.
|
|
48
|
+
const { requestQueue } = await createCrawleeSubFolders(randomToken);
|
|
44
49
|
const crawler = register(new crawlee.PlaywrightCrawler({
|
|
45
50
|
launchContext: {
|
|
46
51
|
launcher: constants.launcher,
|
|
@@ -65,8 +70,10 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
65
70
|
};
|
|
66
71
|
},
|
|
67
72
|
],
|
|
73
|
+
postPageCloseHooks: [getPostPageCloseHook(userDataDirectory)],
|
|
68
74
|
},
|
|
69
75
|
requestList,
|
|
76
|
+
requestQueue,
|
|
70
77
|
maxRequestRetries: 3,
|
|
71
78
|
maxSessionRotations: 1,
|
|
72
79
|
postNavigationHooks: [
|
|
@@ -125,14 +132,22 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
125
132
|
if (isNotSupportedDocument) {
|
|
126
133
|
request.skipNavigation = true;
|
|
127
134
|
request.userData.isNotSupportedDocument = true;
|
|
128
|
-
// Log for verification (optional, but not required for correctness)
|
|
129
|
-
// console.log(`[SKIP] Not supported: ${request.url}`);
|
|
130
135
|
return;
|
|
131
136
|
}
|
|
137
|
+
try {
|
|
138
|
+
const pathname = new URL(url).pathname;
|
|
139
|
+
const ext = pathname.split('.').pop();
|
|
140
|
+
if (ext && blackListedFileExtensions.includes(ext)) {
|
|
141
|
+
request.skipNavigation = true;
|
|
142
|
+
request.userData.isNotSupportedDocument = true;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
132
147
|
},
|
|
133
148
|
],
|
|
134
149
|
requestHandlerTimeoutSecs: 90,
|
|
135
|
-
requestHandler: async ({ page, request, response, sendRequest }) => {
|
|
150
|
+
requestHandler: async ({ page, request, response, sendRequest, enqueueLinks }) => {
|
|
136
151
|
// Log documents that are not supported
|
|
137
152
|
if (request.userData?.isNotSupportedDocument) {
|
|
138
153
|
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
@@ -268,6 +283,34 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
268
283
|
results.url = request.url;
|
|
269
284
|
results.actualUrl = actualUrl;
|
|
270
285
|
await dataset.pushData(results);
|
|
286
|
+
// Discover <a> links from this page for the intelligent sitemap flow.
|
|
287
|
+
// This eliminates the need for a separate crawlDomain supplement phase
|
|
288
|
+
// that would re-visit all these pages just to extract links.
|
|
289
|
+
if (fromCrawlIntelligentSitemap) {
|
|
290
|
+
try {
|
|
291
|
+
await enqueueLinks({
|
|
292
|
+
selector: `a:not(${disallowedSelectorPatterns})`,
|
|
293
|
+
strategy,
|
|
294
|
+
requestQueue,
|
|
295
|
+
transformRequestFunction: (req) => {
|
|
296
|
+
try {
|
|
297
|
+
req.url = req.url.replace(/(?<=&|\?)utm_.*?(&|$)/gim, '');
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
if (isDisallowedInRobotsTxt(req.url))
|
|
301
|
+
return null;
|
|
302
|
+
if (isUrlPdf(req.url)) {
|
|
303
|
+
req.skipNavigation = true;
|
|
304
|
+
}
|
|
305
|
+
req.label = req.url;
|
|
306
|
+
return req;
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Best-effort link discovery; don't fail the scan
|
|
312
|
+
}
|
|
313
|
+
}
|
|
271
314
|
}
|
|
272
315
|
}
|
|
273
316
|
else {
|
|
@@ -276,18 +319,34 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
276
319
|
urlScanned: request.url,
|
|
277
320
|
});
|
|
278
321
|
if (isScanHtml) {
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
// Non-HTML content types (images, PDFs, binary files) and URLs
|
|
323
|
+
// that redirect to non-HTML resources should be classified as
|
|
324
|
+
// unsupported documents, not generic page errors.
|
|
325
|
+
const isNonHtmlContent = contentType &&
|
|
326
|
+
!contentType.startsWith('text/html') &&
|
|
327
|
+
!contentType.includes('html');
|
|
328
|
+
if (isNonHtmlContent && status !== 0) {
|
|
329
|
+
urlsCrawled.userExcluded.push({
|
|
330
|
+
actualUrl,
|
|
331
|
+
url: request.url,
|
|
332
|
+
pageTitle: request.url,
|
|
333
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
334
|
+
httpStatusCode: 1,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const httpStatus = response?.status();
|
|
339
|
+
const metadata = typeof httpStatus === 'number'
|
|
340
|
+
? STATUS_CODE_METADATA[httpStatus] || STATUS_CODE_METADATA[599]
|
|
341
|
+
: STATUS_CODE_METADATA[2];
|
|
342
|
+
urlsCrawled.invalid.push({
|
|
343
|
+
actualUrl,
|
|
344
|
+
url: request.url,
|
|
345
|
+
pageTitle: request.url,
|
|
346
|
+
metadata,
|
|
347
|
+
httpStatusCode: typeof httpStatus === 'number' ? httpStatus : 0,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
291
350
|
}
|
|
292
351
|
}
|
|
293
352
|
}
|
|
@@ -302,7 +361,53 @@ const crawlSitemap = async ({ sitemapUrl, randomToken, host, viewportSettings, m
|
|
|
302
361
|
if (isAbortingScan) {
|
|
303
362
|
return;
|
|
304
363
|
}
|
|
364
|
+
// Handle download-triggered navigation errors: Playwright throws
|
|
365
|
+
// "Download is starting" when page.goto() hits a file download URL.
|
|
366
|
+
const isDownloadError = request.errorMessages?.some((msg) => msg.includes('Download is starting'));
|
|
367
|
+
if (isDownloadError) {
|
|
368
|
+
if (isScanPdfs) {
|
|
369
|
+
// Re-enqueue with skipNavigation so the requestHandler's PDF download path handles it
|
|
370
|
+
try {
|
|
371
|
+
await requestQueue.addRequest({
|
|
372
|
+
url: request.url,
|
|
373
|
+
skipNavigation: true,
|
|
374
|
+
label: request.url,
|
|
375
|
+
uniqueKey: `download_${request.url}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch { }
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
guiInfoLog(guiInfoStatusTypes.SKIPPED, {
|
|
382
|
+
numScanned: urlsCrawled.scanned.length,
|
|
383
|
+
urlScanned: request.url,
|
|
384
|
+
});
|
|
385
|
+
urlsCrawled.userExcluded.push({
|
|
386
|
+
url: request.url,
|
|
387
|
+
pageTitle: request.url,
|
|
388
|
+
actualUrl: request.url,
|
|
389
|
+
metadata: STATUS_CODE_METADATA[1],
|
|
390
|
+
httpStatusCode: 1,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
305
395
|
const status = response?.status();
|
|
396
|
+
// Re-enqueue rate-limited (403) URLs once for a retry after concurrency recovers.
|
|
397
|
+
// Don't call onFailure here — the re-enqueued request gets a fresh attempt.
|
|
398
|
+
// If it fails again (rateLimitRetried=true), it falls through to the normal path.
|
|
399
|
+
if (status === 403 && !request.userData?.rateLimitRetried) {
|
|
400
|
+
try {
|
|
401
|
+
await requestQueue.addRequest({
|
|
402
|
+
url: request.url,
|
|
403
|
+
label: request.url,
|
|
404
|
+
uniqueKey: `ratelimit_${request.url}`,
|
|
405
|
+
userData: { rateLimitRetried: true },
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch { }
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
306
411
|
if (rateController.onFailure(status, crawler.autoscaledPool)) {
|
|
307
412
|
consoleLogger.info(`Aborting crawl: consecutive HTTP failures threshold reached (site may be rate-limiting). Successfully scanned ${urlsCrawled.scanned.length} pages.`);
|
|
308
413
|
isAbortingScan = true;
|
package/dist/mergeAxeResults.js
CHANGED
|
@@ -831,7 +831,7 @@ generateJsonFiles = false, preferredBrowser) => {
|
|
|
831
831
|
consoleLogger.info(`Dataset drop: ${error.message}`);
|
|
832
832
|
}
|
|
833
833
|
try {
|
|
834
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
834
|
+
const requestQueue = await RequestQueue.open(`${crawleeDir}_rq`);
|
|
835
835
|
await requestQueue.drop();
|
|
836
836
|
}
|
|
837
837
|
catch (error) {
|
|
@@ -841,6 +841,7 @@ generateJsonFiles = false, preferredBrowser) => {
|
|
|
841
841
|
const crawleePath = path.join(storagePath, 'crawlee');
|
|
842
842
|
try {
|
|
843
843
|
await fs.promises.rm(crawleePath, { recursive: true, force: true });
|
|
844
|
+
await fs.promises.rm(`${crawleePath}_rq`, { recursive: true, force: true });
|
|
844
845
|
}
|
|
845
846
|
catch {
|
|
846
847
|
// Best-effort; storage was already dropped via API
|
package/dist/utils.js
CHANGED
|
@@ -346,15 +346,34 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
346
346
|
catch (error) {
|
|
347
347
|
consoleLogger.warn(`Unable to force remove userDataDirectory: ${error.message}`);
|
|
348
348
|
}
|
|
349
|
-
// Also remove _pool* sibling directories created by browser pool re-launches
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
// Also remove _pool* sibling directories created by browser pool re-launches.
|
|
350
|
+
// On Windows, Chrome writes files (optimization_guide_model_store,
|
|
351
|
+
// segmentation_platform, first_party_sets.db, etc.) during its shutdown
|
|
352
|
+
// sequence and may still hold file locks. Retry up to 10 times at 5s intervals.
|
|
353
|
+
const removePoolDirs = () => {
|
|
354
|
+
// On Windows, glob interprets backslashes as escape characters, not path
|
|
355
|
+
// separators. Normalize to forward slashes so the pattern actually matches.
|
|
356
|
+
const globPattern = `${constants.userDataDirectory}_pool*`.replace(/\\/g, '/');
|
|
357
|
+
const poolDirs = globSync(globPattern);
|
|
352
358
|
for (const dir of poolDirs) {
|
|
353
359
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
354
360
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
361
|
+
return poolDirs.length;
|
|
362
|
+
};
|
|
363
|
+
for (let attempt = 0; attempt < 10; attempt++) {
|
|
364
|
+
try {
|
|
365
|
+
if (removePoolDirs() === 0)
|
|
366
|
+
break; // nothing left to clean
|
|
367
|
+
break; // rmSync succeeded for all dirs
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
if (attempt < 9) {
|
|
371
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
consoleLogger.warn('Unable to remove pool directories after 10 attempts');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
358
377
|
}
|
|
359
378
|
}
|
|
360
379
|
if (process.env.TMPDIR && fs.existsSync('/.dockerenv'))
|
|
@@ -374,7 +393,7 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
374
393
|
const crawleeDir = path.join(storagePath, 'crawlee');
|
|
375
394
|
const dataset = await Dataset.open(crawleeDir);
|
|
376
395
|
await dataset.drop();
|
|
377
|
-
const requestQueue = await RequestQueue.open(crawleeDir);
|
|
396
|
+
const requestQueue = await RequestQueue.open(`${crawleeDir}_rq`);
|
|
378
397
|
await requestQueue.drop();
|
|
379
398
|
}
|
|
380
399
|
catch (error) {
|
|
@@ -386,6 +405,12 @@ export const cleanUp = async (randomToken, isError = false) => {
|
|
|
386
405
|
catch (error) {
|
|
387
406
|
consoleLogger.warn(`Unable to force remove crawlee folder: ${error.message}`);
|
|
388
407
|
}
|
|
408
|
+
try {
|
|
409
|
+
fs.rmSync(path.join(storagePath, 'crawlee_rq'), { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
consoleLogger.warn(`Unable to force remove crawlee_rq folder: ${error.message}`);
|
|
413
|
+
}
|
|
389
414
|
try {
|
|
390
415
|
fs.rmSync(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
|
391
416
|
}
|
|
@@ -808,9 +833,11 @@ export const zipResults = async (zipName, resultsPath) => {
|
|
|
808
833
|
if (!stats.isDirectory()) {
|
|
809
834
|
throw new Error(`resultsPath is not a directory: ${resultsPath}`);
|
|
810
835
|
}
|
|
811
|
-
async function addFolderToZip(folderPath, zipFolder) {
|
|
836
|
+
async function addFolderToZip(folderPath, zipFolder, isRoot = false) {
|
|
812
837
|
const items = await fs.readdir(folderPath);
|
|
813
838
|
for (const item of items) {
|
|
839
|
+
if (isRoot && item.startsWith('crawlee'))
|
|
840
|
+
continue;
|
|
814
841
|
const fullPath = path.join(folderPath, item);
|
|
815
842
|
const stats = await fs.stat(fullPath);
|
|
816
843
|
if (stats.isDirectory()) {
|
|
@@ -824,7 +851,7 @@ export const zipResults = async (zipName, resultsPath) => {
|
|
|
824
851
|
}
|
|
825
852
|
}
|
|
826
853
|
const zip = new JSZip();
|
|
827
|
-
await addFolderToZip(resultsPath, zip);
|
|
854
|
+
await addFolderToZip(resultsPath, zip, true);
|
|
828
855
|
const zipStream = zip.generateNodeStream({
|
|
829
856
|
type: 'nodebuffer',
|
|
830
857
|
streamFiles: true,
|
package/oobee-client-scanner.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* DO NOT EDIT MANUALLY. Re-generate with: node dist/generateOobeeClientScanner.js
|
|
4
4
|
*
|
|
5
5
|
* Embedded at generation time:
|
|
6
|
-
* App version : 0.
|
|
6
|
+
* App version : 0.11.0
|
|
7
7
|
* Sentry DSN : (from OOBEE_SENTRY_DSN env var or constants.ts default)
|
|
8
8
|
* Sentry SDK : @sentry/browser 10.58.0 (loaded from CDN at runtime)
|
|
9
9
|
*
|
|
@@ -34887,7 +34887,7 @@
|
|
|
34887
34887
|
// ── Sentry browser telemetry (Sentry JS SDK, loaded from CDN) ────────────
|
|
34888
34888
|
|
|
34889
34889
|
var _oobeeSentryDsn = "https://3b8c7ee46b06f33815a1301b6713ebc3@o4509047624761344.ingest.us.sentry.io/4509327783559168";
|
|
34890
|
-
var _oobeeAppVersion = "0.
|
|
34890
|
+
var _oobeeAppVersion = "0.11.0";
|
|
34891
34891
|
var _oobeeSentryVersion = "10.58.0";
|
|
34892
34892
|
var _oobeeSentryInitialized = false;
|
|
34893
34893
|
var _oobeeSentryLoadPromise = null;
|
package/package.json
CHANGED
package/src/combine.ts
CHANGED
|
@@ -96,6 +96,13 @@ const combineRun = async (details: Data, deviceToScan: string) => {
|
|
|
96
96
|
consoleLogger.info(`Suppressed Playwright post-close connection error: ${err.message}`);
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
|
+
// Suppress EPERM errors from Crawlee's async lock-file operations that fire
|
|
100
|
+
// after a crawl phase finishes. On Windows, the sitemap phase's async lock
|
|
101
|
+
// cleanup can race with the domain phase starting in the same directory.
|
|
102
|
+
if (err.message?.includes('EPERM') && err.message?.includes('.json.lock')) {
|
|
103
|
+
consoleLogger.info(`Suppressed Crawlee lock-file EPERM (stale async cleanup): ${err.message}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
99
106
|
throw err;
|
|
100
107
|
};
|
|
101
108
|
process.on('uncaughtException', psTreeHandler);
|
package/src/constants/common.ts
CHANGED
|
@@ -2157,6 +2157,15 @@ export const getPlaywrightLaunchOptions = (browser?: string): LaunchOptions => {
|
|
|
2157
2157
|
!(browser === BrowserTypes.CHROME && arg === '--edge-skip-compat-layer-relaunch'),
|
|
2158
2158
|
);
|
|
2159
2159
|
|
|
2160
|
+
// Cap browser disk cache to 10MB per instance to prevent storage bloat
|
|
2161
|
+
// during long crawls with multiple pool rotations
|
|
2162
|
+
finalArgs.push('--disk-cache-size=10485760');
|
|
2163
|
+
|
|
2164
|
+
// Prevent Windows from throttling background Chromium processes
|
|
2165
|
+
if (os.platform() === 'win32') {
|
|
2166
|
+
finalArgs.push('--disable-features=UseEcoQoSForBackgroundProcess');
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2160
2169
|
// Headless flags (unchanged)
|
|
2161
2170
|
if (process.env.CRAWLEE_HEADLESS === '1') {
|
|
2162
2171
|
if (!finalArgs.includes('--mute-audio')) finalArgs.push('--mute-audio');
|
|
@@ -30,10 +30,27 @@ export const blackListedFileExtensions = [
|
|
|
30
30
|
'svg',
|
|
31
31
|
'gif',
|
|
32
32
|
'woff',
|
|
33
|
+
'woff2',
|
|
33
34
|
'zip',
|
|
34
35
|
'webp',
|
|
35
36
|
'json',
|
|
36
37
|
'xml',
|
|
38
|
+
'ico',
|
|
39
|
+
'bmp',
|
|
40
|
+
'tiff',
|
|
41
|
+
'tif',
|
|
42
|
+
'avi',
|
|
43
|
+
'mov',
|
|
44
|
+
'wmv',
|
|
45
|
+
'flv',
|
|
46
|
+
'ogg',
|
|
47
|
+
'wav',
|
|
48
|
+
'doc',
|
|
49
|
+
'docx',
|
|
50
|
+
'xls',
|
|
51
|
+
'xlsx',
|
|
52
|
+
'ppt',
|
|
53
|
+
'pptx',
|
|
37
54
|
];
|
|
38
55
|
|
|
39
56
|
export const getIntermediateScreenshotsPath = (datasetsPath: string): string =>
|