diff --git a/src/combine.ts b/src/combine.ts index 4624bc85..9d303e68 100644 --- a/src/combine.ts +++ b/src/combine.ts @@ -89,6 +89,13 @@ const combineRun = async (details: Data, deviceToScan: string) => { consoleLogger.info(`Suppressed Crawlee ps-tree locale error: ${err.message}`); return; } + // Suppress stale Playwright in-process connection errors that fire asynchronously + // after the browser is closed in writeSummaryPdf. The deferred IPC message arrives + // via setImmediate after the browser instance has already been disposed. + if (err.message?.includes('was not bound in the connection')) { + consoleLogger.info(`Suppressed Playwright post-close connection error: ${err.message}`); + return; + } throw err; }; process.on('uncaughtException', psTreeHandler); diff --git a/src/constants/common.ts b/src/constants/common.ts index 0cf2908c..95cb991b 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -1497,6 +1497,18 @@ export const getBrowserToRun = ( * after checkingUrl and unable to utilise same cookie for scan * */ export const getClonedProfilesWithRandomToken = (browser: string, randomToken: string): string => { + // In Docker, redirect browser temp files away from /tmp to avoid ENOSPC. + // Keep the path short — Chrome creates Unix sockets inside TMPDIR-based paths, + // and socket paths are limited to 107 bytes on Linux. + if (fs.existsSync('/.dockerenv')) { + const baseDir = getDefaultChromiumDataDir(); + if (baseDir) { + const scanTmpDir = path.join(baseDir, 'tmp'); + fs.mkdirSync(scanTmpDir, { recursive: true }); + process.env.TMPDIR = scanTmpDir; + } + } + if (browser === BrowserTypes.CHROME) { return cloneChromeProfiles(randomToken); } diff --git a/src/generateHtmlReport.ts b/src/generateHtmlReport.ts index a8cb6ba7..25f60b75 100644 --- a/src/generateHtmlReport.ts +++ b/src/generateHtmlReport.ts @@ -126,6 +126,18 @@ export const generateHtmlReport = async ( const scanData = JSON.parse(await fs.readFile(scanDataJsonPath, 'utf8')); const scanItemsAll = JSON.parse(await fs.readFile(scanItemsJsonPath, 'utf8')); + // Disk space: passed may be absent from scanItems.json (excluded to reduce disk usage). + // Provide an empty-category fallback so convertItemsToReferences does not crash. + // To revert (when passed is restored in scanItems.json), remove this block. + if (!scanItemsAll.passed) { + scanItemsAll.passed = { + description: itemTypeDescription.passed, + totalItems: 0, + totalRuleIssues: 0, + rules: [], + }; + } + // Build the lighter scanItems payload used by the HTML report. const lightScanItemsPayload = convertItemsToReferences({ items: scanItemsAll, @@ -141,7 +153,7 @@ export const generateHtmlReport = async ( mustFix: ensureCategory(mustFix, 'mustFix'), goodToFix: ensureCategory(goodToFix, 'goodToFix'), needsReview: ensureCategory(needsReview, 'needsReview'), - passed: ensureCategory(scanItemsAll.passed || {}, 'passed'), + passed: ensureCategory(scanItemsAll.passed, 'passed'), }; const pagesScanned = Array.isArray(scanData.pagesScanned) ? scanData.pagesScanned : []; diff --git a/src/logs.ts b/src/logs.ts index 67e6fdcd..edf8738a 100644 --- a/src/logs.ts +++ b/src/logs.ts @@ -39,6 +39,10 @@ export const errorsTxtPath = path.join(basePath, `${uuid}.txt`); const consoleLogger = createLogger({ silent: !(process.env.RUNNING_FROM_PH_GUI || process.env.OOBEE_VERBOSE), + // exitOnError: false — Winston logs uncaught exceptions but does not call process.exit(). + // Process lifecycle is controlled by psTreeHandler in combine.ts, which suppresses known + // benign errors (e.g. Playwright post-close connection errors) and rethrows real ones. + exitOnError: false, format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat), transports: [ new transports.Console({ level: 'info' }), @@ -54,6 +58,8 @@ const consoleLogger = createLogger({ // Also used in common functions to not link internal information // if running from mass scanner, log out errors in console const silentLogger = createLogger({ + // exitOnError: false — see consoleLogger comment above. + exitOnError: false, format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat), transports: [ new transports.File({ diff --git a/src/mergeAxeResults.ts b/src/mergeAxeResults.ts index a9b8ba2e..685b5b2f 100644 --- a/src/mergeAxeResults.ts +++ b/src/mergeAxeResults.ts @@ -359,6 +359,11 @@ const writeSummaryPdf = async ( browser: string, _userDataDirectory: string, ) => { + // Flush stale browser temp files before launching PDF browser + if (process.env.TMPDIR) { + fs.emptyDirSync(process.env.TMPDIR); + } + const renderPdfWithBrowser = async (browserToUse: string) => { let browserInstance; let context; @@ -525,26 +530,40 @@ const pushResults = async (pageResults, allIssues, isCustomFlow, itemsStore: Ite metadata, itemsCount: items.length, }; - await itemsStore.appendPageItems(category, rule, { - url, - pageTitle, - items, - pageIndex, - pageImagePath, - metadata, - }); + // Disk space: skip storing passed items in itemsStore. + // To revert, remove the `if` guard and use the original call directly: + // await itemsStore.appendPageItems(category, rule, { + // url, pageTitle, items, pageIndex, pageImagePath, metadata, + // }); + if (category !== 'passed') { + await itemsStore.appendPageItems(category, rule, { + url, + pageTitle, + items, + pageIndex, + pageImagePath, + metadata, + }); + } } else if (!(url in currRuleFromAllIssues.pagesAffected)) { currRuleFromAllIssues.pagesAffected[url] = { pageTitle, itemsCount: items.length, ...(filePath && { filePath }), }; - await itemsStore.appendPageItems(category, rule, { - url, - pageTitle, - items, - ...(filePath && { filePath }), - }); + // Disk space: skip storing passed items in itemsStore. + // To revert, remove the `if` guard and use the original call directly: + // await itemsStore.appendPageItems(category, rule, { + // url, pageTitle, items, ...(filePath && { filePath }), + // }); + if (category !== 'passed') { + await itemsStore.appendPageItems(category, rule, { + url, + pageTitle, + items, + ...(filePath && { filePath }), + }); + } } } } @@ -924,7 +943,9 @@ const generateArtifacts = async ( populateScanPagesDetail(allIssues); // Build htmlGroups in a second pass from disk-backed items - for (const category of ['mustFix', 'goodToFix', 'needsReview', 'passed'] as const) { + // Disk space: 'passed' removed from this loop (no items stored in itemsStore for passed). + // To revert, restore: ['mustFix', 'goodToFix', 'needsReview', 'passed'] + for (const category of ['mustFix', 'goodToFix', 'needsReview'] as const) { for (const rule of allIssues.items[category].rules) { for await (const entry of itemsStore.readRuleItems(category, rule.rule)) { buildHtmlGroups(rule, entry.items, entry.url); diff --git a/src/mergeAxeResults/jsonArtifacts.ts b/src/mergeAxeResults/jsonArtifacts.ts index 15074f30..3c56d9b8 100644 --- a/src/mergeAxeResults/jsonArtifacts.ts +++ b/src/mergeAxeResults/jsonArtifacts.ts @@ -292,9 +292,14 @@ const writeJsonAndBase64Files = async ( const { items, ...rest } = allIssues; const { jsonFilePath: scanDataJsonFilePath, base64FilePath: scanDataBase64FilePath } = await writeJsonFileAndCompressedJsonFile(rest, storagePath, 'scanData'); + // Disk space: passed items excluded from scanItems.json to reduce disk usage. + // Passed counts are still in scanData.json and the embedded report payload (scanItems-light). + // To revert, remove the destructure line and restore the original argument: + // { oobeeAppVersion: allIssues.oobeeAppVersion, ...items } + const { passed: _passedItems, ...itemsWithoutPassed } = items; const { jsonFilePath: scanItemsJsonFilePath, base64FilePath: scanItemsBase64FilePath } = await writeJsonFileAndCompressedJsonFile( - { oobeeAppVersion: allIssues.oobeeAppVersion, ...items }, + { oobeeAppVersion: allIssues.oobeeAppVersion, ...itemsWithoutPassed }, storagePath, 'scanItems', itemsStore, diff --git a/src/utils.ts b/src/utils.ts index a06658d6..c22a2434 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -388,6 +388,12 @@ export const cleanUp = async (randomToken?: string, isError: boolean = false): P consoleLogger.warn(`Unable to force remove userDataDirectory: ${error.message}`); } + if (process.env.TMPDIR) try { + fs.rmSync(process.env.TMPDIR, { recursive: true, force: true }); + } catch (error) { + consoleLogger.warn(`Unable to force remove browser tmp dir: ${error.message}`); + } + if (randomToken !== undefined) { const storagePath = getStoragePath(randomToken);