From 9b0acb2d85e02d2dc23feff681c050b65b5f584d Mon Sep 17 00:00:00 2001 From: Zui Young Date: Thu, 2 Jul 2026 16:46:56 +0800 Subject: [PATCH 1/5] improv: omit passed items from disk writes to reduce storage usage - mergeAxeResults.ts: skip itemsStore.appendPageItems for 'passed' category in pushResults (both custom flow and standard flow paths) - mergeAxeResults.ts: remove 'passed' from buildHtmlGroups loop (no items stored in itemsStore for passed; convertItemsToReferences already excludes htmlGroups for passed) - jsonArtifacts.ts: exclude passed from scanItems.json write payload; passed counts remain available via scanData.json and the embedded scanItems-light payload in the HTML report - generateHtmlReport.ts: add fallback empty-category for passed when regenerating a report from scanItems.json that lacks the passed key All changes annotated with revert instructions. --- src/generateHtmlReport.ts | 14 ++++++++- src/mergeAxeResults.ts | 46 +++++++++++++++++++--------- src/mergeAxeResults/jsonArtifacts.ts | 7 ++++- 3 files changed, 50 insertions(+), 17 deletions(-) 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/mergeAxeResults.ts b/src/mergeAxeResults.ts index a9b8ba2e..49b9147c 100644 --- a/src/mergeAxeResults.ts +++ b/src/mergeAxeResults.ts @@ -525,26 +525,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 +938,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, From 1654c7ce150bf8711b62c984d87f4fd9b3b3347f Mon Sep 17 00:00:00 2001 From: Zui Young Date: Thu, 2 Jul 2026 17:36:52 +0800 Subject: [PATCH 2/5] fix: suppress Playwright post-close connection error in psTreeHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After writeSummaryPdf closes the browser, a deferred setImmediate callback fires from Playwright's in-process IPC factory attempting to dispatch a message to an already-disposed Response object. This arrives as an uncaughtException through psTreeHandler and crashes the process. Suppress 'was not bound in the connection' errors the same way the Crawlee ps-tree locale error is suppressed — they are benign post-cleanup artifacts and do not affect scan output. --- src/combine.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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); From 6dd6713813cddaaf425c7c8b2810533fefa94a44 Mon Sep 17 00:00:00 2001 From: Zui Young Date: Thu, 2 Jul 2026 17:42:16 +0800 Subject: [PATCH 3/5] fix: set exitOnError: false on Winston loggers to prevent premature exit Winston's handleExceptions: true registers its own uncaughtException listener that fires before psTreeHandler and calls process.exit(1) by default. This caused the Playwright post-close 'not bound in the connection' error to exit the process before psTreeHandler could suppress it. Setting exitOnError: false on both consoleLogger and silentLogger makes Winston log the exception without exiting, allowing psTreeHandler in combine.ts to decide whether to continue (benign errors) or rethrow (real errors that should terminate the process). --- src/logs.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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({ From 1cb840da2bf86b0efe250aeefe097e6906b32e00 Mon Sep 17 00:00:00 2001 From: younglim Date: Fri, 3 Jul 2026 15:57:54 +0800 Subject: [PATCH 4/5] fix: redirect browser TMPDIR to scan's user data dir to prevent /tmp ENOSPC in Docker In constrained containers, Chrome's --disable-dev-shm-usage writes shared memory to /tmp, exhausting it before PDF generation can launch a second browser. This redirects TMPDIR into the scan's existing Chromium Support directory (scoped by randomToken), flushes it before PDF browser launch, and cleans it up on exit. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/constants/common.ts | 10 ++++++++++ src/mergeAxeResults.ts | 5 +++++ src/utils.ts | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/src/constants/common.ts b/src/constants/common.ts index 0cf2908c..47e68e94 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -1497,6 +1497,16 @@ 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 into the scan's user data directory to avoid /tmp ENOSPC + if (fs.existsSync('/.dockerenv')) { + const baseDir = getDefaultChromiumDataDir(); + if (baseDir) { + const scanTmpDir = path.join(baseDir, `oobee-${randomToken}`, 'tmp'); + fs.mkdirSync(scanTmpDir, { recursive: true }); + process.env.TMPDIR = scanTmpDir; + } + } + if (browser === BrowserTypes.CHROME) { return cloneChromeProfiles(randomToken); } diff --git a/src/mergeAxeResults.ts b/src/mergeAxeResults.ts index 49b9147c..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; 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); From b8b9ecd323601447a887e300e60ba3d73a398a36 Mon Sep 17 00:00:00 2001 From: younglim Date: Fri, 3 Jul 2026 17:36:46 +0800 Subject: [PATCH 5/5] fix: shorten TMPDIR path to avoid exceeding Unix socket 107-byte limit The previous path included the full randomToken (e.g. oobee-20260703_..._997/tmp), which when combined with Playwright's subdirectory exceeded Linux's 107-byte Unix socket path limit, causing Chrome to crash with SIGTRAP on launch. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/constants/common.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants/common.ts b/src/constants/common.ts index 47e68e94..95cb991b 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -1497,11 +1497,13 @@ 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 into the scan's user data directory to avoid /tmp ENOSPC + // 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, `oobee-${randomToken}`, 'tmp'); + const scanTmpDir = path.join(baseDir, 'tmp'); fs.mkdirSync(scanTmpDir, { recursive: true }); process.env.TMPDIR = scanTmpDir; }