@fanboynz/network-scanner
Version:
A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.
523 lines (443 loc) • 19.2 kB
JavaScript
/**
* Browser exit and cleanup handler module
* Provides graceful and forced browser closure functionality with comprehensive temp file cleanup
*/
// Constants for temp file cleanup
const CHROME_TEMP_PATHS = [
'/tmp',
'/dev/shm',
'/tmp/snap-private-tmp/snap.chromium/tmp'
];
const CHROME_TEMP_PATTERNS = [
'.com.google.Chrome.*', // Google Chrome temp files
'.org.chromium.Chromium.*',
'puppeteer-*',
'.com.google.Chrome.*' // Ensure Google Chrome pattern is included
];
/**
* Clean Chrome temporary files and directories
* @param {Object} options - Cleanup options
* @param {boolean} options.includeSnapTemp - Whether to clean snap temp directories
* @param {boolean} options.forceDebug - Whether to output debug logs
* @param {boolean} options.comprehensive - Whether to perform comprehensive cleanup of all temp locations
* @returns {Promise<Object>} Cleanup results
*/
async function cleanupChromeTempFiles(options = {}) {
const {
includeSnapTemp = false,
forceDebug = false,
comprehensive = false
} = options;
try {
const { execSync } = require('child_process');
// Base cleanup commands for standard temp directories
const cleanupCommands = [
'rm -rf /tmp/.com.google.Chrome.* 2>/dev/null || true',
'rm -rf /tmp/.org.chromium.Chromium.* 2>/dev/null || true',
'rm -rf /tmp/puppeteer-* 2>/dev/null || true',
'rm -rf /dev/shm/.com.google.Chrome.* 2>/dev/null || true',
'rm -rf /dev/shm/.org.chromium.Chromium.* 2>/dev/null || true'
];
// Add snap-specific cleanup if requested
if (includeSnapTemp || comprehensive) {
cleanupCommands.push(
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/.org.chromium.Chromium.* 2>/dev/null || true',
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/puppeteer-* 2>/dev/null || true'
);
}
let totalCleaned = 0;
for (const command of cleanupCommands) {
try {
// Get file count before cleanup for reporting
const listCommand = command.replace('rm -rf', 'ls -1d').replace(' 2>/dev/null || true', ' 2>/dev/null | wc -l || echo 0');
const fileCount = parseInt(execSync(listCommand, { stdio: 'pipe' }).toString().trim()) || 0;
if (fileCount > 0) {
execSync(command, { stdio: 'ignore' });
totalCleaned += fileCount;
if (forceDebug) {
const pathPattern = command.match(/rm -rf ([^ ]+)/)?.[1] || 'unknown';
console.log(`[debug] [temp-cleanup] Cleaned ${fileCount} items from ${pathPattern}`);
}
}
} catch (cmdErr) {
// Ignore individual command errors but log in debug mode
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Cleanup command failed: ${command} (${cmdErr.message})`);
}
}
}
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Standard cleanup completed (${totalCleaned} items)`);
}
return { success: true, itemsCleaned: totalCleaned };
} catch (cleanupErr) {
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Chrome cleanup error: ${cleanupErr.message}`);
}
return { success: false, error: cleanupErr.message, itemsCleaned: 0 };
}
}
/**
* Comprehensive temp file cleanup that systematically checks all known Chrome temp locations
* @param {Object} options - Cleanup options
* @param {boolean} options.forceDebug - Whether to output debug logs
* @param {boolean} options.verbose - Whether to show verbose output
* @returns {Promise<Object>} Cleanup results
*/
async function comprehensiveChromeTempCleanup(options = {}) {
const { forceDebug = false, verbose = false } = options;
try {
const { execSync } = require('child_process');
let totalCleaned = 0;
if (verbose && !forceDebug) {
console.log(`[temp-cleanup] Scanning Chrome/Puppeteer temporary files...`);
}
for (const basePath of CHROME_TEMP_PATHS) {
// Check if the base path exists before trying to clean it
try {
const pathExists = execSync(`test -d "${basePath}" && echo "exists" || echo "missing"`, { stdio: 'pipe' })
.toString().trim() === 'exists';
if (!pathExists) {
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Skipping non-existent path: ${basePath}`);
}
continue;
}
for (const pattern of CHROME_TEMP_PATTERNS) {
const fullPattern = `${basePath}/${pattern}`;
// Count items before deletion
const countCommand = `ls -1d ${fullPattern} 2>/dev/null | wc -l || echo 0`;
const itemCount = parseInt(execSync(countCommand, { stdio: 'pipe' }).toString().trim()) || 0;
if (itemCount > 0) {
const deleteCommand = `rm -rf ${fullPattern} 2>/dev/null || true`;
execSync(deleteCommand, { stdio: 'ignore' });
totalCleaned += itemCount;
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Removed ${itemCount} items matching ${fullPattern}`);
}
}
}
} catch (pathErr) {
if (forceDebug) {
console.log(`[debug] [temp-cleanup] Error checking path ${basePath}: ${pathErr.message}`);
}
}
}
if (verbose && totalCleaned > 0) {
console.log(`[temp-cleanup] ? Removed ${totalCleaned} temporary file(s)/folder(s)`);
} else if (verbose && totalCleaned === 0) {
console.log(`[temp-cleanup] ?? No temporary files found to remove`);
} else if (forceDebug) {
console.log(`[debug] [temp-cleanup] Comprehensive cleanup completed (${totalCleaned} items)`);
}
return { success: true, itemsCleaned: totalCleaned };
} catch (err) {
const errorMsg = `Comprehensive temp file cleanup failed: ${err.message}`;
if (verbose) {
console.warn(`[temp-cleanup] ? ${errorMsg}`);
} else if (forceDebug) {
console.log(`[debug] [temp-cleanup] ${errorMsg}`);
}
return { success: false, error: err.message, itemsCleaned: 0 };
}
}
/**
* Cleanup specific user data directory (for browser instances)
* @param {string} userDataDir - Path to user data directory to clean
* @param {boolean} forceDebug - Whether to output debug logs
* @returns {Promise<Object>} Cleanup results
*/
async function cleanupUserDataDir(userDataDir, forceDebug = false) {
if (!userDataDir) {
return { success: true, cleaned: false, reason: 'No user data directory specified' };
}
try {
const fs = require('fs');
if (!fs.existsSync(userDataDir)) {
if (forceDebug) {
console.log(`[debug] [user-data] User data directory does not exist: ${userDataDir}`);
}
return { success: true, cleaned: false, reason: 'Directory does not exist' };
}
fs.rmSync(userDataDir, { recursive: true, force: true });
if (forceDebug) {
console.log(`[debug] [user-data] Cleaned user data directory: ${userDataDir}`);
}
return { success: true, cleaned: true };
} catch (rmErr) {
if (forceDebug) {
console.log(`[debug] [user-data] Failed to remove user data directory ${userDataDir}: ${rmErr.message}`);
}
return { success: false, error: rmErr.message, cleaned: false };
}
}
/**
* Attempts to gracefully close all browser pages and the browser instance
* @param {import('puppeteer').Browser} browser - The Puppeteer browser instance
* @param {boolean} forceDebug - Whether to output debug logs
* @returns {Promise<void>}
*/
async function gracefulBrowserCleanup(browser, forceDebug = false) {
if (forceDebug) console.log(`[debug] [browser] Getting all browser pages...`);
const pages = await browser.pages();
if (forceDebug) console.log(`[debug] [browser] Found ${pages.length} pages to close`);
await Promise.all(pages.map(async (page) => {
if (!page.isClosed()) {
try {
if (forceDebug) console.log(`[debug] [browser] Closing page: ${page.url()}`);
await page.close();
if (forceDebug) console.log(`[debug] [browser] Page closed successfully`);
} catch (err) {
// Force close if normal close fails
if (forceDebug) console.log(`[debug] [browser] Force closing page: ${err.message}`);
}
}
}));
if (forceDebug) console.log(`[debug] [browser] All pages closed, closing browser...`);
await browser.close();
if (forceDebug) console.log(`[debug] [browser] Browser closed successfully`);
}
/**
* Force kills the browser process using system signals
* @param {import('puppeteer').Browser} browser - The Puppeteer browser instance
* @param {boolean} forceDebug - Whether to output debug logs
* @returns {Promise<void>}
*/
async function forceBrowserKill(browser, forceDebug = false) {
try {
if (forceDebug) console.log(`[debug] [browser] Attempting force closure of browser process...`);
const browserProcess = browser.process();
if (!browserProcess || !browserProcess.pid) {
if (forceDebug) console.log(`[debug] [browser] No browser process available`);
return;
}
const mainPid = browserProcess.pid;
if (forceDebug) console.log(`[debug] [browser] Main Chrome PID: ${mainPid}`);
// Find and kill ALL related Chrome processes
const { execSync } = require('child_process');
try {
// Find all Chrome processes with puppeteer in command line
const psCmd = `ps -eo pid,cmd | grep "puppeteer.*chrome" | grep -v grep`;
const psOutput = execSync(psCmd, { encoding: 'utf8', timeout: 5000 });
const lines = psOutput.trim().split('\n').filter(line => line.trim());
const pidsToKill = [];
for (const line of lines) {
const match = line.trim().match(/^\s*(\d+)/);
if (match) {
const pid = parseInt(match[1]);
if (!isNaN(pid)) {
pidsToKill.push(pid);
}
}
}
if (forceDebug) {
console.log(`[debug] [browser] Found ${pidsToKill.length} Chrome processes to kill: [${pidsToKill.join(', ')}]`);
}
// Kill all processes with SIGTERM first (graceful)
for (const pid of pidsToKill) {
try {
process.kill(pid, 'SIGTERM');
if (forceDebug) console.log(`[debug] [browser] Sent SIGTERM to PID ${pid}`);
} catch (killErr) {
if (forceDebug) console.log(`[debug] [browser] Failed to send SIGTERM to PID ${pid}: ${killErr.message}`);
}
}
// Wait for graceful termination
await new Promise(resolve => setTimeout(resolve, 3000));
// Force kill any remaining processes with SIGKILL
for (const pid of pidsToKill) {
try {
// Check if process still exists using signal 0
process.kill(pid, 0);
// If we reach here, process still exists - force kill it
process.kill(pid, 'SIGKILL');
if (forceDebug) console.log(`[debug] [browser] Force killed PID ${pid} with SIGKILL`);
} catch (checkErr) {
// Process already dead (ESRCH error is expected and good)
if (forceDebug && checkErr.code !== 'ESRCH') {
console.log(`[debug] [browser] Error checking/killing PID ${pid}: ${checkErr.message}`);
}
}
}
// Final verification - check if any processes are still alive
if (forceDebug) {
try {
const verifyCmd = `ps -eo pid,cmd | grep "puppeteer.*chrome" | grep -v grep | wc -l`;
const remainingCount = execSync(verifyCmd, { encoding: 'utf8', timeout: 2000 }).trim();
console.log(`[debug] [browser] Remaining Chrome processes after cleanup: ${remainingCount}`);
} catch (verifyErr) {
console.log(`[debug] [browser] Could not verify process cleanup: ${verifyErr.message}`);
}
}
} catch (psErr) {
// Fallback to original method if ps command fails
if (forceDebug) console.log(`[debug] [browser] ps command failed, using fallback method: ${psErr.message}`);
try {
browserProcess.kill('SIGTERM');
await new Promise(resolve => setTimeout(resolve, 2000));
// Check if main process still exists and force kill if needed
try {
process.kill(mainPid, 0); // Check existence
browserProcess.kill('SIGKILL'); // Force kill if still alive
if (forceDebug) console.log(`[debug] [browser] Fallback: Force killed main PID ${mainPid}`);
} catch (checkErr) {
if (forceDebug && checkErr.code !== 'ESRCH') {
console.log(`[debug] [browser] Fallback check error for PID ${mainPid}: ${checkErr.message}`);
}
}
} catch (fallbackErr) {
if (forceDebug) console.log(`[debug] [browser] Fallback kill failed: ${fallbackErr.message}`);
}
}
} catch (forceKillErr) {
console.error(`[error] [browser] Failed to force kill browser: ${forceKillErr.message}`);
}
try {
if (browser.isConnected()) {
browser.disconnect();
if (forceDebug) console.log(`[debug] [browser] Browser connection disconnected`);
}
} catch (disconnectErr) {
if (forceDebug) console.log(`[debug] [browser] Failed to disconnect browser: ${disconnectErr.message}`);
}
}
/**
* Kill all Chrome processes by command line pattern (nuclear option)
* @param {boolean} forceDebug - Whether to output debug logs
* @returns {Promise<void>}
*/
async function killAllPuppeteerChrome(forceDebug = false) {
try {
const { execSync } = require('child_process');
if (forceDebug) console.log(`[debug] [browser] Nuclear option: killing all puppeteer Chrome processes...`);
try {
execSync(`pkill -f "puppeteer.*chrome"`, { stdio: 'ignore', timeout: 5000 });
if (forceDebug) console.log(`[debug] [browser] pkill completed`);
} catch (pkillErr) {
if (forceDebug && pkillErr.status !== 1) {
console.log(`[debug] [browser] pkill failed with status ${pkillErr.status}: ${pkillErr.message}`);
}
}
await new Promise(resolve => setTimeout(resolve, 2000));
} catch (nuclearErr) {
console.error(`[error] [browser] Nuclear Chrome kill failed: ${nuclearErr.message}`);
}
}
/**
* Handles comprehensive browser cleanup including processes, temp files, and user data
* @param {import('puppeteer').Browser} browser - The Puppeteer browser instance
* @param {Object} options - Cleanup options
* @param {boolean} options.forceDebug - Whether to output debug logs
* @param {number} options.timeout - Timeout in milliseconds before force closure (default: 10000)
* @param {boolean} options.exitOnFailure - Whether to exit process on cleanup failure (default: true)
* @param {boolean} options.cleanTempFiles - Whether to clean standard temp files (default: true)
* @param {boolean} options.comprehensiveCleanup - Whether to perform comprehensive temp file cleanup (default: false)
* @param {string} options.userDataDir - User data directory to clean (optional)
* @param {boolean} options.verbose - Whether to show verbose cleanup output (default: false)
* @returns {Promise<Object>} - Returns cleanup results object
*/
async function handleBrowserExit(browser, options = {}) {
const {
forceDebug = false,
timeout = 10000,
exitOnFailure = true,
cleanTempFiles = true,
comprehensiveCleanup = false,
userDataDir = null,
verbose = false
} = options;
if (forceDebug) console.log(`[debug] [browser] Starting comprehensive browser cleanup...`);
const results = {
browserClosed: false,
tempFilescleaned: 0,
userDataCleaned: false,
success: false,
errors: []
};
try {
// Step 1: Browser process cleanup
try {
// Race cleanup against timeout
await Promise.race([
gracefulBrowserCleanup(browser, forceDebug),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Browser cleanup timeout')), timeout)
)
]);
results.browserClosed = true;
} catch (browserCloseErr) {
results.errors.push(`Browser cleanup failed: ${browserCloseErr.message}`);
if (forceDebug || verbose) {
console.warn(`[warn] [browser] Browser cleanup had issues: ${browserCloseErr.message}`);
}
// Attempt force kill
await forceBrowserKill(browser, forceDebug);
// Nuclear option if force kill didn't work
if (forceDebug) console.log(`[debug] [browser] Attempting nuclear cleanup...`);
await killAllPuppeteerChrome(forceDebug);
results.browserClosed = true; // Assume success after nuclear option
}
// Step 2: User data directory cleanup
if (userDataDir) {
const userDataResult = await cleanupUserDataDir(userDataDir, forceDebug);
results.userDataCleaned = userDataResult.cleaned;
if (!userDataResult.success) {
results.errors.push(`User data cleanup failed: ${userDataResult.error}`);
}
}
// Step 3: Temp file cleanup
if (cleanTempFiles) {
if (comprehensiveCleanup) {
const tempResult = await comprehensiveChromeTempCleanup({ forceDebug, verbose });
results.tempFilesCleanedSuccess = tempResult.success;
results.tempFilesCleanedComprehensive = true;
if (tempResult.success) {
results.tempFilesCleanedCount = tempResult.itemsCleaned;
} else {
results.errors.push(`Comprehensive temp cleanup failed: ${tempResult.error}`);
}
} else {
const tempResult = await cleanupChromeTempFiles({
includeSnapTemp: true,
forceDebug,
comprehensive: false
});
results.tempFilesCleanedSuccess = tempResult.success;
if (tempResult.success) {
results.tempFilesCleanedCount = tempResult.itemsCleaned;
} else {
results.errors.push(`Standard temp cleanup failed: ${tempResult.error}`);
}
}
}
// Determine overall success
results.success = results.browserClosed &&
(results.errors.length === 0 || !exitOnFailure);
if (forceDebug) {
console.log(`[debug] [browser] Cleanup completed - Browser: ${results.browserClosed}, ` +
`Temp files: ${results.tempFilesCleanedCount || 0}, ` +
`User data: ${results.userDataCleaned}, ` +
`Errors: ${results.errors.length}`);
}
return results;
} catch (overallErr) {
results.errors.push(`Overall cleanup failed: ${overallErr.message}`);
results.success = false;
if (exitOnFailure) {
if (forceDebug) console.log(`[debug] [browser] Forcing process exit due to cleanup failure`);
process.exit(1);
}
return results;
}
}
module.exports = {
handleBrowserExit,
gracefulBrowserCleanup,
forceBrowserKill,
killAllPuppeteerChrome,
cleanupChromeTempFiles,
comprehensiveChromeTempCleanup,
cleanupUserDataDir,
CHROME_TEMP_PATHS,
CHROME_TEMP_PATTERNS
};