@fanboynz/network-scanner 2.0.56 → 2.0.58
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/.github/workflows/npm-publish.yml +4 -5
- package/CLAUDE.md +65 -0
- package/lib/adblock.js +4 -3
- package/lib/browserexit.js +61 -96
- package/lib/browserhealth.js +223 -183
- package/lib/compare.js +0 -4
- package/lib/compress.js +6 -15
- package/lib/dry-run.js +1 -1
- package/lib/fingerprint.js +39 -37
- package/lib/flowproxy.js +8 -8
- package/lib/grep.js +1 -1
- package/lib/ignore_similar.js +78 -209
- package/lib/interaction.js +23 -45
- package/lib/openvpn_vpn.js +16 -21
- package/lib/output.js +12 -6
- package/lib/post-processing.js +282 -356
- package/lib/smart-cache.js +347 -267
- package/lib/validate_rules.js +12 -27
- package/nwss.js +15 -3
- package/package.json +3 -2
- package/.clauderc +0 -30
package/lib/browserhealth.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const { formatLogMessage, messageColors } = require('./colorize');
|
|
7
|
-
const { execSync } = require('child_process');
|
|
7
|
+
const { execSync, execFile } = require('child_process');
|
|
8
8
|
|
|
9
9
|
// Window cleanup delay constant
|
|
10
10
|
const WINDOW_CLEANUP_DELAY_MS = 15000;
|
|
@@ -20,15 +20,35 @@ const pageCreationTracker = new Map(); // Maps page -> creation timestamp
|
|
|
20
20
|
const pageUsageTracker = new Map(); // Maps page -> { lastActivity: timestamp, isProcessing: boolean }
|
|
21
21
|
const PAGE_IDLE_THRESHOLD = 25000; // 25 seconds of inactivity before considering page safe to clean
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Race a promise against a timeout, clearing the timer when the promise resolves/rejects.
|
|
25
|
+
* Prevents leaked setTimeout handles that hold closure references until they fire.
|
|
26
|
+
* @param {Promise} promise - The operation to race
|
|
27
|
+
* @param {number} ms - Timeout in milliseconds
|
|
28
|
+
* @param {string} msg - Error message on timeout
|
|
29
|
+
* @returns {Promise} Resolves/rejects with the operation result, or rejects on timeout
|
|
30
|
+
*/
|
|
31
|
+
function raceWithTimeout(promise, ms, msg) {
|
|
32
|
+
let timeoutId;
|
|
33
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
34
|
+
timeoutId = setTimeout(() => reject(new Error(msg)), ms);
|
|
35
|
+
});
|
|
36
|
+
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const BYTES_GB = 1073741824; // 1024^3
|
|
40
|
+
const BYTES_MB = 1048576; // 1024^2
|
|
41
|
+
const BYTES_KB = 1024;
|
|
42
|
+
|
|
23
43
|
/**
|
|
24
44
|
* Format bytes to human readable string
|
|
25
45
|
* @param {number} bytes
|
|
26
46
|
* @returns {string}
|
|
27
47
|
*/
|
|
28
48
|
function formatMemory(bytes) {
|
|
29
|
-
if (bytes >=
|
|
30
|
-
if (bytes >=
|
|
31
|
-
if (bytes >=
|
|
49
|
+
if (bytes >= BYTES_GB) return `${(bytes / BYTES_GB).toFixed(1)}GB`;
|
|
50
|
+
if (bytes >= BYTES_MB) return `${(bytes / BYTES_MB).toFixed(1)}MB`;
|
|
51
|
+
if (bytes >= BYTES_KB) return `${(bytes / BYTES_KB).toFixed(1)}KB`;
|
|
32
52
|
return `${bytes}B`;
|
|
33
53
|
}
|
|
34
54
|
|
|
@@ -80,9 +100,6 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
80
100
|
}
|
|
81
101
|
} else {
|
|
82
102
|
// Any page with actual content should be evaluated for closure
|
|
83
|
-
// Cache page state checks to avoid multiple browser calls
|
|
84
|
-
const isPageClosed = page.isClosed();
|
|
85
|
-
|
|
86
103
|
if (cleanupMode === "all") {
|
|
87
104
|
// Aggressive mode: close all content pages
|
|
88
105
|
pagesToClose.push(page);
|
|
@@ -119,49 +136,31 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
119
136
|
return result;
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
// Estimate memory usage before closing
|
|
139
|
+
// Estimate memory usage before closing (parallel for performance)
|
|
123
140
|
let totalEstimatedMemory = 0;
|
|
124
|
-
const
|
|
141
|
+
const DEFAULT_PAGE_MEMORY = 8 * 1024 * 1024; // 8MB default estimate
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
const page = pagesToClose[i];
|
|
128
|
-
let pageMemoryEstimate = 0;
|
|
129
|
-
|
|
143
|
+
const pageMemoryEstimates = await Promise.all(pagesToClose.map(async (page) => {
|
|
130
144
|
try {
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Puppeteer metrics provide various memory-related values
|
|
143
|
-
pageMemoryEstimate = (
|
|
144
|
-
(metrics.JSHeapUsedSize || 0) + // JavaScript heap
|
|
145
|
-
(metrics.JSHeapTotalSize || 0) * 0.1 + // Estimated overhead
|
|
146
|
-
(metrics.Nodes || 0) * 100 + // DOM nodes (rough estimate)
|
|
147
|
-
(metrics.JSEventListeners || 0) * 50 // Event listeners
|
|
148
|
-
);
|
|
149
|
-
} else {
|
|
150
|
-
// Fallback: rough estimate based on page complexity
|
|
151
|
-
pageMemoryEstimate = 8 * 1024 * 1024; // 8MB default estimate per page
|
|
152
|
-
}
|
|
145
|
+
if (page.isClosed()) return 0;
|
|
146
|
+
const metrics = await raceWithTimeout(
|
|
147
|
+
page.metrics(), 1000, 'metrics timeout'
|
|
148
|
+
);
|
|
149
|
+
if (metrics) {
|
|
150
|
+
return (
|
|
151
|
+
(metrics.JSHeapUsedSize || 0) +
|
|
152
|
+
(metrics.JSHeapTotalSize || 0) * 0.1 +
|
|
153
|
+
(metrics.Nodes || 0) * 100 +
|
|
154
|
+
(metrics.JSEventListeners || 0) * 50
|
|
155
|
+
);
|
|
153
156
|
}
|
|
157
|
+
return DEFAULT_PAGE_MEMORY;
|
|
154
158
|
} catch (metricsErr) {
|
|
155
|
-
|
|
156
|
-
pageMemoryEstimate = 8 * 1024 * 1024; // 8MB default
|
|
157
|
-
if (forceDebug) {
|
|
158
|
-
console.log(formatLogMessage('debug', `[group_window_cleanup] Could not get metrics for page ${i + 1}, using default estimate: ${metricsErr.message}`));
|
|
159
|
-
}
|
|
159
|
+
return DEFAULT_PAGE_MEMORY;
|
|
160
160
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
totalEstimatedMemory = pageMemoryEstimates.reduce((sum, mem) => sum + mem, 0);
|
|
165
164
|
|
|
166
165
|
// Close identified old/unused pages
|
|
167
166
|
const closePromises = pagesToClose.map(async (page, index) => {
|
|
@@ -188,10 +187,15 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
188
187
|
});
|
|
189
188
|
|
|
190
189
|
const closeResults = await Promise.all(closePromises);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
// Single-pass count + sum (avoids 2 intermediate array allocations from .filter())
|
|
191
|
+
let successfulCloses = 0;
|
|
192
|
+
let actualMemoryFreed = 0;
|
|
193
|
+
for (let i = 0; i < closeResults.length; i++) {
|
|
194
|
+
if (closeResults[i].success === true) {
|
|
195
|
+
successfulCloses++;
|
|
196
|
+
actualMemoryFreed += closeResults[i].estimatedMemory || 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
195
199
|
|
|
196
200
|
if (forceDebug) {
|
|
197
201
|
console.log(formatLogMessage('debug', `[group_window_cleanup] Closed ${successfulCloses}/${pagesToClose.length} old windows for completed group: ${groupDescription} after ${WINDOW_CLEANUP_DELAY_MS}ms delay`));
|
|
@@ -241,39 +245,33 @@ async function performGroupWindowCleanup(browserInstance, groupDescription, forc
|
|
|
241
245
|
*/
|
|
242
246
|
async function isPageSafeToClose(page, forceDebug) {
|
|
243
247
|
try {
|
|
244
|
-
|
|
245
|
-
const isPageClosed = page.isClosed();
|
|
246
|
-
if (isPageClosed) {
|
|
248
|
+
if (page.isClosed()) {
|
|
247
249
|
return true; // Already closed
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
// EXTRA SAFETY: Never close pages that might be in injection process
|
|
253
|
+
const now = Date.now();
|
|
251
254
|
try {
|
|
252
|
-
// Cache page.url() for safety checks
|
|
253
255
|
const pageUrl = page.url();
|
|
254
|
-
if (pageUrl && pageUrl !== 'about:blank' &&
|
|
256
|
+
if (pageUrl && pageUrl !== 'about:blank' && now - (pageCreationTracker.get(page) || 0) < 30000) {
|
|
255
257
|
return false; // Don't close recently created pages (within 30 seconds)
|
|
256
258
|
}
|
|
257
259
|
} catch (err) { /* ignore */ }
|
|
258
260
|
|
|
259
261
|
const usage = pageUsageTracker.get(page);
|
|
260
262
|
if (!usage) {
|
|
261
|
-
// No usage data - assume safe if page exists for a while
|
|
262
263
|
return true;
|
|
263
264
|
}
|
|
264
265
|
|
|
265
|
-
// Check if page is actively processing
|
|
266
266
|
if (usage.isProcessing) {
|
|
267
267
|
if (forceDebug) {
|
|
268
|
-
// Cache URL for debug logging
|
|
269
268
|
const pageUrl = page.url();
|
|
270
269
|
console.log(formatLogMessage('debug', `[realtime_cleanup] Page still processing: ${pageUrl.substring(0, 50)}...`));
|
|
271
270
|
}
|
|
272
271
|
return false;
|
|
273
272
|
}
|
|
274
273
|
|
|
275
|
-
|
|
276
|
-
const idleTime = Date.now() - usage.lastActivity;
|
|
274
|
+
const idleTime = now - usage.lastActivity;
|
|
277
275
|
const isSafe = idleTime >= PAGE_IDLE_THRESHOLD;
|
|
278
276
|
|
|
279
277
|
if (!isSafe && forceDebug) {
|
|
@@ -296,13 +294,15 @@ async function isPageSafeToClose(page, forceDebug) {
|
|
|
296
294
|
*/
|
|
297
295
|
function updatePageUsage(page, isProcessing = false) {
|
|
298
296
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
lastActivity
|
|
304
|
-
isProcessing
|
|
305
|
-
}
|
|
297
|
+
if (!page.isClosed()) {
|
|
298
|
+
const existing = pageUsageTracker.get(page);
|
|
299
|
+
if (existing) {
|
|
300
|
+
// Mutate in place -- same hidden class, no GC pressure
|
|
301
|
+
existing.lastActivity = Date.now();
|
|
302
|
+
existing.isProcessing = isProcessing;
|
|
303
|
+
} else {
|
|
304
|
+
pageUsageTracker.set(page, { lastActivity: Date.now(), isProcessing: isProcessing });
|
|
305
|
+
}
|
|
306
306
|
}
|
|
307
307
|
} catch (err) {
|
|
308
308
|
// Ignore errors in usage tracking
|
|
@@ -320,8 +320,6 @@ function updatePageUsage(page, isProcessing = false) {
|
|
|
320
320
|
*/
|
|
321
321
|
async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIME_CLEANUP_THRESHOLD, forceDebug, totalDelay = 19000) {
|
|
322
322
|
try {
|
|
323
|
-
const allPages = await browserInstance.pages();
|
|
324
|
-
|
|
325
323
|
// Initialize result object with consistent shape
|
|
326
324
|
const result = {
|
|
327
325
|
success: false,
|
|
@@ -334,13 +332,22 @@ async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIM
|
|
|
334
332
|
error: null
|
|
335
333
|
};
|
|
336
334
|
|
|
335
|
+
// Quick count check before waiting (avoid the expensive delay if unnecessary)
|
|
336
|
+
let quickPages;
|
|
337
|
+
try {
|
|
338
|
+
quickPages = await browserInstance.pages();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
result.error = e.message;
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
337
344
|
// Skip cleanup if we don't have enough pages to warrant it
|
|
338
|
-
if (
|
|
345
|
+
if (quickPages.length <= Math.max(threshold, REALTIME_CLEANUP_MIN_PAGES)) {
|
|
339
346
|
if (forceDebug) {
|
|
340
|
-
console.log(formatLogMessage('debug', `[realtime_cleanup] Only ${
|
|
347
|
+
console.log(formatLogMessage('debug', `[realtime_cleanup] Only ${quickPages.length} pages open, threshold is ${threshold} - no cleanup needed`));
|
|
341
348
|
}
|
|
342
349
|
result.success = true;
|
|
343
|
-
result.totalPages =
|
|
350
|
+
result.totalPages = quickPages.length;
|
|
344
351
|
result.reason = 'below_threshold';
|
|
345
352
|
return result;
|
|
346
353
|
}
|
|
@@ -431,11 +438,12 @@ async function performRealtimeWindowCleanup(browserInstance, threshold = REALTIM
|
|
|
431
438
|
let closedCount = 0;
|
|
432
439
|
for (const page of safePagesToClose) {
|
|
433
440
|
try {
|
|
434
|
-
// Cache both page state and URL for this iteration
|
|
435
441
|
const isPageClosed = page.isClosed();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
|
|
443
|
+
// Re-check processing state — may have changed since safety check
|
|
444
|
+
const usage = pageUsageTracker.get(page);
|
|
445
|
+
if (!isPageClosed && !(usage && usage.isProcessing)) {
|
|
446
|
+
const pageUrl = page.url();
|
|
439
447
|
await page.close();
|
|
440
448
|
pageCreationTracker.delete(page); // Remove from tracker
|
|
441
449
|
pageUsageTracker.delete(page);
|
|
@@ -509,23 +517,35 @@ async function isPageFromPreviousScan(page, forceDebug) {
|
|
|
509
517
|
return false; // Don't close blank pages here, handled separately
|
|
510
518
|
}
|
|
511
519
|
|
|
512
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
//
|
|
517
|
-
if (title.includes('404') ||
|
|
518
|
-
title.includes('Error') ||
|
|
519
|
-
title.includes('Not Found') ||
|
|
520
|
-
title === '') {
|
|
521
|
-
return true;
|
|
522
|
-
}
|
|
523
|
-
} catch (titleErr) {
|
|
524
|
-
// If we can't get title, page might be in bad state
|
|
520
|
+
// Use tracker timestamp instead of expensive page.title() CDP call
|
|
521
|
+
const now = Date.now();
|
|
522
|
+
const createdAt = pageCreationTracker.get(page);
|
|
523
|
+
if (createdAt && now - createdAt > 120000) {
|
|
524
|
+
// Page is older than 2 minutes -- likely from a previous scan
|
|
525
525
|
return true;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
-
//
|
|
528
|
+
// Check usage tracker -- idle pages are likely old
|
|
529
|
+
const usage = pageUsageTracker.get(page);
|
|
530
|
+
if (usage && !usage.isProcessing && now - usage.lastActivity > 60000) {
|
|
531
|
+
return true; // Idle for over 60 seconds
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Fallback: only use page.title() if trackers have no data
|
|
535
|
+
if (!createdAt && !usage) {
|
|
536
|
+
try {
|
|
537
|
+
const title = await page.title();
|
|
538
|
+
if (title.includes('404') ||
|
|
539
|
+
title.includes('Error') ||
|
|
540
|
+
title.includes('Not Found') ||
|
|
541
|
+
title === '') {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
} catch (titleErr) {
|
|
545
|
+
return true; // Can't get title = bad state
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
529
549
|
return false; // Conservative - don't close unless we're sure
|
|
530
550
|
} catch (err) {
|
|
531
551
|
if (forceDebug) {
|
|
@@ -550,6 +570,35 @@ function trackPageForRealtime(page) {
|
|
|
550
570
|
updatePageUsage(page, false); // Initialize usage tracking
|
|
551
571
|
}
|
|
552
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Removes a page from all tracking Maps immediately.
|
|
575
|
+
* Call this before page.close() to prevent stale entries during concurrent execution.
|
|
576
|
+
* @param {import('puppeteer').Page} page - Page to untrack
|
|
577
|
+
*/
|
|
578
|
+
function untrackPage(page) {
|
|
579
|
+
pageCreationTracker.delete(page);
|
|
580
|
+
pageUsageTracker.delete(page);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Purges stale entries from tracking Maps (pages that were closed without cleanup)
|
|
585
|
+
* Should be called periodically to prevent memory leaks
|
|
586
|
+
*/
|
|
587
|
+
function purgeStaleTrackers() {
|
|
588
|
+
for (const [page] of pageCreationTracker) {
|
|
589
|
+
try {
|
|
590
|
+
if (page.isClosed()) {
|
|
591
|
+
pageCreationTracker.delete(page);
|
|
592
|
+
pageUsageTracker.delete(page);
|
|
593
|
+
}
|
|
594
|
+
} catch (e) {
|
|
595
|
+
// Page reference is invalid, remove it
|
|
596
|
+
pageCreationTracker.delete(page);
|
|
597
|
+
pageUsageTracker.delete(page);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
553
602
|
/**
|
|
554
603
|
* Quick browser responsiveness test for use during page setup
|
|
555
604
|
* Designed to catch browser degradation between operations
|
|
@@ -559,12 +608,11 @@ function trackPageForRealtime(page) {
|
|
|
559
608
|
*/
|
|
560
609
|
async function isQuicklyResponsive(browserInstance, timeout = 3000) {
|
|
561
610
|
try {
|
|
562
|
-
await
|
|
563
|
-
browserInstance.version(),
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
]);
|
|
611
|
+
await raceWithTimeout(
|
|
612
|
+
browserInstance.version(),
|
|
613
|
+
timeout,
|
|
614
|
+
'Quick responsiveness timeout'
|
|
615
|
+
);
|
|
568
616
|
return true;
|
|
569
617
|
} catch (error) {
|
|
570
618
|
return false;
|
|
@@ -590,20 +638,18 @@ async function testNetworkCapability(browserInstance, timeout = 10000) {
|
|
|
590
638
|
|
|
591
639
|
try {
|
|
592
640
|
// Create test page
|
|
593
|
-
testPage = await
|
|
641
|
+
testPage = await raceWithTimeout(
|
|
594
642
|
browserInstance.newPage(),
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
]);
|
|
643
|
+
timeout,
|
|
644
|
+
'Test page creation timeout'
|
|
645
|
+
);
|
|
599
646
|
|
|
600
647
|
// Test network operations (the critical operation that's failing)
|
|
601
|
-
await
|
|
648
|
+
await raceWithTimeout(
|
|
602
649
|
testPage.setRequestInterception(true),
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
]);
|
|
650
|
+
timeout,
|
|
651
|
+
'Network.enable test timeout'
|
|
652
|
+
);
|
|
607
653
|
|
|
608
654
|
// Turn off interception and close
|
|
609
655
|
await testPage.setRequestInterception(false);
|
|
@@ -662,12 +708,11 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
662
708
|
}
|
|
663
709
|
|
|
664
710
|
// Test 2: Try to get pages list with timeout
|
|
665
|
-
const pages = await
|
|
711
|
+
const pages = await raceWithTimeout(
|
|
666
712
|
browserInstance.pages(),
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
]);
|
|
713
|
+
timeout,
|
|
714
|
+
'Browser unresponsive - pages() timeout'
|
|
715
|
+
);
|
|
671
716
|
|
|
672
717
|
healthResult.pageCount = pages.length;
|
|
673
718
|
healthResult.responseTime = Date.now() - startTime;
|
|
@@ -677,23 +722,38 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
677
722
|
healthResult.recommendations.push('Too many open pages - consider browser restart');
|
|
678
723
|
}
|
|
679
724
|
|
|
680
|
-
// Test 4:
|
|
725
|
+
// Test 4: Create a single test page to verify both browser functionality AND network capability
|
|
681
726
|
let testPage = null;
|
|
682
727
|
try {
|
|
683
|
-
testPage = await
|
|
728
|
+
testPage = await raceWithTimeout(
|
|
684
729
|
browserInstance.newPage(),
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
]);
|
|
730
|
+
timeout,
|
|
731
|
+
'Page creation timeout'
|
|
732
|
+
);
|
|
689
733
|
|
|
690
734
|
// Quick test navigation to about:blank
|
|
691
|
-
await
|
|
735
|
+
await raceWithTimeout(
|
|
692
736
|
testPage.goto('about:blank'),
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
737
|
+
timeout,
|
|
738
|
+
'Navigation timeout'
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
// Test 5: Network capability test on the same page (avoids creating a second test page)
|
|
742
|
+
try {
|
|
743
|
+
await raceWithTimeout(
|
|
744
|
+
testPage.setRequestInterception(true),
|
|
745
|
+
Math.min(timeout, 5000),
|
|
746
|
+
'Network.enable test timeout'
|
|
747
|
+
);
|
|
748
|
+
await testPage.setRequestInterception(false);
|
|
749
|
+
healthResult.networkCapable = true;
|
|
750
|
+
} catch (networkErr) {
|
|
751
|
+
healthResult.networkCapable = false;
|
|
752
|
+
healthResult.recommendations.push(`Network operations failing: ${networkErr.message}`);
|
|
753
|
+
if (networkErr.message.includes('Network.enable')) {
|
|
754
|
+
healthResult.criticalError = true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
697
757
|
|
|
698
758
|
await testPage.close();
|
|
699
759
|
|
|
@@ -711,24 +771,13 @@ async function checkBrowserHealth(browserInstance, timeout = 8000) {
|
|
|
711
771
|
return healthResult;
|
|
712
772
|
}
|
|
713
773
|
|
|
714
|
-
// Test 5: Network capability test (critical for Network.enable issues)
|
|
715
|
-
const networkTest = await testNetworkCapability(browserInstance, Math.min(timeout, 5000));
|
|
716
|
-
healthResult.networkCapable = networkTest.capable;
|
|
717
|
-
|
|
718
|
-
if (!networkTest.capable) {
|
|
719
|
-
healthResult.recommendations.push(`Network operations failing: ${networkTest.error}`);
|
|
720
|
-
if (networkTest.error && networkTest.error.includes('Network.enable')) {
|
|
721
|
-
healthResult.criticalError = true;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
774
|
// Test 6: Check response time performance
|
|
726
775
|
if (healthResult.responseTime > 5000) {
|
|
727
776
|
healthResult.recommendations.push('Slow browser response - consider restart');
|
|
728
777
|
}
|
|
729
778
|
|
|
730
779
|
// If all tests pass (including network capability)
|
|
731
|
-
healthResult.healthy =
|
|
780
|
+
healthResult.healthy = healthResult.networkCapable; // Network capability is now critical for health
|
|
732
781
|
|
|
733
782
|
|
|
734
783
|
} catch (error) {
|
|
@@ -782,7 +831,12 @@ async function checkBrowserMemory(browserInstance) {
|
|
|
782
831
|
|
|
783
832
|
// Try to get process memory info (Linux/Unix)
|
|
784
833
|
try {
|
|
785
|
-
const memInfo =
|
|
834
|
+
const memInfo = await new Promise((resolve, reject) => {
|
|
835
|
+
execFile('ps', ['-p', String(browserProcess.pid), '-o', 'rss='], { encoding: 'utf8', timeout: 2000 }, (err, stdout) => {
|
|
836
|
+
if (err) reject(err);
|
|
837
|
+
else resolve(stdout);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
786
840
|
const memoryKB = parseInt(memInfo.trim());
|
|
787
841
|
|
|
788
842
|
if (!isNaN(memoryKB)) {
|
|
@@ -811,40 +865,22 @@ async function checkBrowserMemory(browserInstance) {
|
|
|
811
865
|
return memoryResult;
|
|
812
866
|
}
|
|
813
867
|
|
|
814
|
-
|
|
868
|
+
/**
|
|
869
|
+
* Precompiled regex for critical protocol error detection (avoids array allocation per call)
|
|
870
|
+
*/
|
|
871
|
+
const CRITICAL_ERROR_REGEX = /Runtime\.callFunctionOn timed out|Protocol error|Target closed|Session closed|Connection closed|Browser has been closed|Runtime\.evaluate timed out|WebSocket is not open|WebSocket connection lost|Connection terminated|Network service crashed|Browser disconnected|CDP session invalid|Browser process exited|Navigation timeout of|Page crashed|Renderer process crashed|Network\.enable timed out|Network\.disable timed out|Network service not available/;
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Precompiled regex for restart recommendation detection
|
|
875
|
+
*/
|
|
876
|
+
const RESTART_RECOMMENDATION_REGEX = /restart required|High memory usage/;
|
|
877
|
+
|
|
878
|
+
/**
|
|
815
879
|
* Detects critical protocol errors that require immediate browser restart
|
|
816
880
|
*/
|
|
817
881
|
function isCriticalProtocolError(error) {
|
|
818
882
|
if (!error || !error.message) return false;
|
|
819
|
-
|
|
820
|
-
const criticalErrors = [
|
|
821
|
-
'Runtime.callFunctionOn timed out',
|
|
822
|
-
'Protocol error',
|
|
823
|
-
'Target closed',
|
|
824
|
-
'Session closed',
|
|
825
|
-
'Connection closed',
|
|
826
|
-
'Browser has been closed',
|
|
827
|
-
'Runtime.evaluate timed out',
|
|
828
|
-
// New Puppeteer 23.x critical errors
|
|
829
|
-
'WebSocket is not open',
|
|
830
|
-
'WebSocket connection lost',
|
|
831
|
-
'Connection terminated',
|
|
832
|
-
'Network service crashed',
|
|
833
|
-
'Browser disconnected',
|
|
834
|
-
'CDP session invalid',
|
|
835
|
-
'Browser process exited',
|
|
836
|
-
'Navigation timeout of',
|
|
837
|
-
'Page crashed',
|
|
838
|
-
'Renderer process crashed',
|
|
839
|
-
// Network-specific critical errors
|
|
840
|
-
'Network.enable timed out',
|
|
841
|
-
'Network.disable timed out',
|
|
842
|
-
'Network service not available'
|
|
843
|
-
];
|
|
844
|
-
|
|
845
|
-
return criticalErrors.some(criticalError =>
|
|
846
|
-
error.message.includes(criticalError)
|
|
847
|
-
);
|
|
883
|
+
return CRITICAL_ERROR_REGEX.test(error.message);
|
|
848
884
|
}
|
|
849
885
|
|
|
850
886
|
/**
|
|
@@ -871,12 +907,11 @@ async function testBrowserConnectivity(browserInstance, timeout = 2500) {
|
|
|
871
907
|
|
|
872
908
|
// Test 2: CDP responsiveness with version check
|
|
873
909
|
try {
|
|
874
|
-
const version = await
|
|
910
|
+
const version = await raceWithTimeout(
|
|
875
911
|
browserInstance.version(),
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
]);
|
|
912
|
+
timeout,
|
|
913
|
+
'CDP version check timeout'
|
|
914
|
+
);
|
|
880
915
|
|
|
881
916
|
connectivityResult.cdpResponsive = true;
|
|
882
917
|
connectivityResult.websocketHealthy = true; // If version works, WebSocket is healthy
|
|
@@ -936,12 +971,16 @@ async function performHealthAssessment(browserInstance, options = {}) {
|
|
|
936
971
|
assessment.memory = await checkBrowserMemory(browserInstance);
|
|
937
972
|
}
|
|
938
973
|
|
|
939
|
-
// Combine recommendations
|
|
940
|
-
assessment.recommendations =
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
974
|
+
// Combine recommendations (push avoids spread operator intermediate arrays)
|
|
975
|
+
assessment.recommendations = assessment.browser.recommendations.slice();
|
|
976
|
+
if (assessment.connectivity.error) {
|
|
977
|
+
assessment.recommendations.push(`Connectivity issue: ${assessment.connectivity.error}`);
|
|
978
|
+
}
|
|
979
|
+
if (assessment.memory.recommendations) {
|
|
980
|
+
for (let i = 0; i < assessment.memory.recommendations.length; i++) {
|
|
981
|
+
assessment.recommendations.push(assessment.memory.recommendations[i]);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
945
984
|
|
|
946
985
|
// Determine overall health and restart necessity
|
|
947
986
|
if (!assessment.browser.healthy) {
|
|
@@ -955,10 +994,7 @@ async function performHealthAssessment(browserInstance, options = {}) {
|
|
|
955
994
|
assessment.needsRestart = true;
|
|
956
995
|
} else if (assessment.recommendations.length > 0) {
|
|
957
996
|
assessment.overall = 'degraded';
|
|
958
|
-
assessment.needsRestart = assessment.recommendations.
|
|
959
|
-
rec.includes('restart required') ||
|
|
960
|
-
rec.includes('High memory usage')
|
|
961
|
-
);
|
|
997
|
+
assessment.needsRestart = RESTART_RECOMMENDATION_REGEX.test(assessment.recommendations.join('|'));
|
|
962
998
|
} else {
|
|
963
999
|
assessment.overall = 'healthy';
|
|
964
1000
|
assessment.needsRestart = false;
|
|
@@ -1132,17 +1168,19 @@ async function cleanupPageBeforeReload(page, forceDebug = false) {
|
|
|
1132
1168
|
} catch(e) {}
|
|
1133
1169
|
});
|
|
1134
1170
|
|
|
1135
|
-
// Clear
|
|
1171
|
+
// Clear recent timers and intervals (cap to last 1000 to avoid massive loops)
|
|
1136
1172
|
const highestId = setTimeout(() => {}, 0);
|
|
1137
|
-
|
|
1173
|
+
const clearFrom = Math.max(0, highestId - 1000);
|
|
1174
|
+
for (let i = highestId; i >= clearFrom; i--) {
|
|
1138
1175
|
clearTimeout(i);
|
|
1139
1176
|
clearInterval(i);
|
|
1140
1177
|
}
|
|
1141
1178
|
|
|
1142
|
-
// Stop
|
|
1179
|
+
// Stop recent animations
|
|
1143
1180
|
if (typeof cancelAnimationFrame !== 'undefined') {
|
|
1144
1181
|
const highestRAF = requestAnimationFrame(() => {});
|
|
1145
|
-
|
|
1182
|
+
const clearRAFFrom = Math.max(0, highestRAF - 200);
|
|
1183
|
+
for (let i = highestRAF; i >= clearRAFFrom; i--) {
|
|
1146
1184
|
cancelAnimationFrame(i);
|
|
1147
1185
|
}
|
|
1148
1186
|
}
|
|
@@ -1198,5 +1236,7 @@ module.exports = {
|
|
|
1198
1236
|
isBrowserHealthy,
|
|
1199
1237
|
isCriticalProtocolError,
|
|
1200
1238
|
updatePageUsage,
|
|
1201
|
-
|
|
1239
|
+
untrackPage,
|
|
1240
|
+
cleanupPageBeforeReload,
|
|
1241
|
+
purgeStaleTrackers
|
|
1202
1242
|
};
|
package/lib/compare.js
CHANGED
|
@@ -9,10 +9,6 @@ const path = require('path');
|
|
|
9
9
|
*/
|
|
10
10
|
function loadComparisonRules(compareFilePath, forceDebug = false) {
|
|
11
11
|
try {
|
|
12
|
-
if (!fs.existsSync(compareFilePath)) {
|
|
13
|
-
throw new Error(`Comparison file not found: ${compareFilePath}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
const content = fs.readFileSync(compareFilePath, 'utf8');
|
|
17
13
|
const lines = content.split('\n')
|
|
18
14
|
.map(line => line.trim())
|