From 8fdbd075e18c0e97c7c104f2de1a787af06a9861 Mon Sep 17 00:00:00 2001 From: Farrel Athaillah Date: Thu, 11 Jun 2026 22:43:46 +0000 Subject: [PATCH 1/2] (fix) : MacOs chrome for testing not closing --- src/automation/BrowserManager.ts | 45 ++++++++++++++++++++++-- src/automation/PageController.ts | 60 ++++++++++++++++++++++++++------ 2 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/automation/BrowserManager.ts b/src/automation/BrowserManager.ts index 215aa0f5..490dee1b 100644 --- a/src/automation/BrowserManager.ts +++ b/src/automation/BrowserManager.ts @@ -26,6 +26,8 @@ type BrowserCandidate = BrowserChannel | undefined class BrowserManager { private readonly bot: MicrosoftRewardsBot + private detectedBrowserChannel?: Promise + private static readonly DETECTION_CLOSE_TIMEOUT_MS = 5_000 private static readonly BROWSER_ARGS = [ '--no-sandbox', '--mute-audio', @@ -66,22 +68,61 @@ class BrowserManager { * Edge can block account.live.com on some Windows installations. */ private async detectBrowserChannel(): Promise { + if (this.detectedBrowserChannel) { + return this.detectedBrowserChannel + } + + this.detectedBrowserChannel = this.detectBrowserChannelOnce().catch(error => { + this.detectedBrowserChannel = undefined + throw error + }) + + return this.detectedBrowserChannel + } + + private async detectBrowserChannelOnce(): Promise { for (const channel of [undefined, 'chrome', 'msedge'] as const) { + let testBrowser: rebrowser.Browser | undefined try { - const testBrowser = await rebrowser.chromium.launch({ + testBrowser = await rebrowser.chromium.launch({ headless: true, ...(channel && { channel }) }) - await testBrowser.close() return channel } catch { // Channel not available, try next + } finally { + if (testBrowser?.isConnected()) { + await this.closeDetectionBrowser(testBrowser).catch(() => {}) + } } } throw new Error('No supported Chromium browser found. Run `npx patchright install chromium` and try again.') } + private async closeDetectionBrowser(browser: rebrowser.Browser): Promise { + let timeout: NodeJS.Timeout | undefined + try { + await Promise.race([ + browser.close(), + new Promise((_, reject) => { + timeout = setTimeout( + () => + reject( + new Error( + `Browser detection close timed out after ${BrowserManager.DETECTION_CLOSE_TIMEOUT_MS}ms` + ) + ), + BrowserManager.DETECTION_CLOSE_TIMEOUT_MS + ) + }) + ]) + } finally { + if (timeout) clearTimeout(timeout) + } + } + async createBrowser(account: Account): Promise { let browser: rebrowser.Browser let channel: BrowserChannel | undefined diff --git a/src/automation/PageController.ts b/src/automation/PageController.ts index 59ee8361..bf34d174 100644 --- a/src/automation/PageController.ts +++ b/src/automation/PageController.ts @@ -1,5 +1,5 @@ import type { AxiosRequestConfig } from 'axios' -import type { BrowserContext, Cookie, Page } from 'patchright' +import type { Browser, BrowserContext, Cookie, Page } from 'patchright' import type { StorageOrigin } from '../helpers/ConfigLoader' import { saveSessionData, saveStorageState } from '../helpers/ConfigLoader' @@ -14,6 +14,7 @@ import { URLS } from './DashboardSelectors' export default class PageController { private bot: MicrosoftRewardsBot + private static readonly BROWSER_CLOSE_TIMEOUT_MS = 10_000 constructor(bot: MicrosoftRewardsBot) { this.bot = bot @@ -83,7 +84,9 @@ export default class PageController { try { const page = this.bot.isMobile ? this.bot.mainMobilePage : this.bot.mainDesktopPage - await page.goto(this.bot.config.baseURL, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {}) + await page + .goto(this.bot.config.baseURL, { waitUntil: 'domcontentloaded', timeout: 30_000 }) + .catch(() => {}) const html = await page.content() return this.parseDashboardHtml(html) } catch (fallbackError) { @@ -96,7 +99,11 @@ export default class PageController { private parseDashboardHtml(html: string): DashboardData { const legacyMatch = html.match(/var\s+dashboard\s*=\s*({.*?});/s) if (legacyMatch?.[1]) { - this.bot.logger.debug(this.bot.isMobile, 'GET-DASHBOARD-DATA', 'Extracted dashboard data from legacy HTML embed') + this.bot.logger.debug( + this.bot.isMobile, + 'GET-DASHBOARD-DATA', + 'Extracted dashboard data from legacy HTML embed' + ) return JSON.parse(legacyMatch[1]) as DashboardData } @@ -163,9 +170,7 @@ export default class PageController { const parsed = JSON.parse(callArgs) as unknown if (!Array.isArray(parsed)) return null - return parsed - .filter((part): part is string => typeof part === 'string') - .join('\n') + return parsed.filter((part): part is string => typeof part === 'string').join('\n') } catch { const stringValues: string[] = [] const stringPattern = /"((?:\\.|[^"\\])*)"/gs @@ -561,9 +566,12 @@ export default class PageController { } } - async closeBrowser(browser: BrowserContext, email: string) { + async closeBrowser(context: BrowserContext, email: string) { + let parentBrowser: Browser | null = null + try { - const cookies = await browser.cookies() + parentBrowser = context.browser() + const cookies = await context.cookies() // Save cookies this.bot.logger.debug( @@ -576,7 +584,7 @@ export default class PageController { // Save localStorage from all open pages (rewards.bing.com, bing.com) try { const storageOrigins: StorageOrigin[] = [] - const pages = browser.pages() + const pages = context.pages() const seenOrigins = new Set() for (const page of pages) { @@ -625,10 +633,20 @@ export default class PageController { await this.bot.utils.wait(2000) - // Close browser - await browser.close() + await this.closeWithTimeout(() => context.close(), 'context') + const browserToClose = parentBrowser + if (browserToClose?.isConnected()) { + await this.closeWithTimeout(() => browserToClose.close(), 'browser') + } this.bot.logger.info(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!') } catch (error) { + const browserToClose = parentBrowser + await Promise.allSettled([ + this.closeWithTimeout(() => context.close(), 'context').catch(() => {}), + browserToClose?.isConnected() + ? this.closeWithTimeout(() => browserToClose.close(), 'browser').catch(() => {}) + : Promise.resolve() + ]) this.bot.logger.error( this.bot.isMobile, 'CLOSE-BROWSER', @@ -638,6 +656,26 @@ export default class PageController { } } + private async closeWithTimeout(close: () => Promise, label: string): Promise { + let timeout: NodeJS.Timeout | undefined + try { + await Promise.race([ + close(), + new Promise((_, reject) => { + timeout = setTimeout( + () => + reject( + new Error(`${label} close timed out after ${PageController.BROWSER_CLOSE_TIMEOUT_MS}ms`) + ), + PageController.BROWSER_CLOSE_TIMEOUT_MS + ) + }) + ]) + } finally { + if (timeout) clearTimeout(timeout) + } + } + /** * Report an activity completion via `fetch()` executed inside the browser page. * From ad0f2400c5006d7ebedfa3fd26d33b006c0113aa Mon Sep 17 00:00:00 2001 From: Farrel Athaillah Date: Sat, 13 Jun 2026 17:52:34 +0000 Subject: [PATCH 2/2] fix: rebase & resolving some conflict --- src/automation/BrowserManager.ts | 61 +++++++------------------------- src/automation/PageController.ts | 17 ++------- tests/browser-runtime.test.js | 4 +++ 3 files changed, 18 insertions(+), 64 deletions(-) diff --git a/src/automation/BrowserManager.ts b/src/automation/BrowserManager.ts index 54175e41..0141c267 100644 --- a/src/automation/BrowserManager.ts +++ b/src/automation/BrowserManager.ts @@ -23,12 +23,9 @@ interface BrowserCreationResult { fingerprint: BrowserFingerprintWithHeaders } -type BrowserChannel = 'chrome' | 'msedge' - class BrowserManager { private readonly bot: MicrosoftRewardsBot - private detectedBrowserChannel?: Promise - private static readonly DETECTION_CLOSE_TIMEOUT_MS = 5_000 + private static readonly BROWSER_CLOSE_TIMEOUT_MS = 10_000 private readonly activeBrowsers = new Set() private static readonly BROWSER_ARGS = [ '--no-sandbox', @@ -66,40 +63,6 @@ class BrowserManager { this.bot = bot } - /** - * Attempts to find the best Chromium-based browser. - * Preference: Patchright bundled Chromium > Google Chrome > Microsoft Edge. - * Edge can block account.live.com on some Windows installations. - */ - private async detectBrowserChannel(): Promise { - if (this.detectedBrowserChannel) { - return this.detectedBrowserChannel - } - - this.detectedBrowserChannel = this.detectBrowserChannelOnce().catch(error => { - this.detectedBrowserChannel = undefined - throw error - }) - - return this.detectedBrowserChannel - } - - private async detectBrowserChannelOnce(): Promise { - for (const channel of [undefined, 'chrome', 'msedge'] as const) { - let testBrowser: rebrowser.Browser | undefined - try { - testBrowser = await rebrowser.chromium.launch({ - headless: true, - ...(channel && { channel }) - }) - return channel - } catch { - // Channel not available, try next - } finally { - if (testBrowser?.isConnected()) { - await this.closeDetectionBrowser(testBrowser).catch(() => {}) - } - } private async assertBundledChromiumAvailable(): Promise { try { const testBrowser = await rebrowser.chromium.launch({ headless: true }) @@ -111,7 +74,7 @@ class BrowserManager { } } - private async closeDetectionBrowser(browser: rebrowser.Browser): Promise { + private async closeBrowserWithTimeout(browser: rebrowser.Browser): Promise { let timeout: NodeJS.Timeout | undefined try { await Promise.race([ @@ -121,10 +84,10 @@ class BrowserManager { () => reject( new Error( - `Browser detection close timed out after ${BrowserManager.DETECTION_CLOSE_TIMEOUT_MS}ms` + `Browser close timed out after ${BrowserManager.BROWSER_CLOSE_TIMEOUT_MS}ms` ) ), - BrowserManager.DETECTION_CLOSE_TIMEOUT_MS + BrowserManager.BROWSER_CLOSE_TIMEOUT_MS ) }) ]) @@ -135,12 +98,11 @@ class BrowserManager { async createBrowser(account: Account): Promise { let browser: rebrowser.Browser - let channel: BrowserChannel | undefined try { this.bot.logger.info( this.bot.isMobile, 'BROWSER', - 'Initializing browser — detecting available channel (Chromium › Chrome › Edge)...' + 'Initializing browser — verifying Patchright bundled Chromium...' ) const proxyConfig = account.proxy.url @@ -182,8 +144,7 @@ class BrowserManager { ) const fingerprint = - sessionData.fingerprint ?? - (await this.generateFingerprint(this.bot.isMobile, channel === 'msedge' ? 'edge' : 'chrome')) + sessionData.fingerprint ?? (await this.generateFingerprint(this.bot.isMobile, 'chrome')) const context = await newInjectedContext(browser as any, { fingerprint, @@ -192,9 +153,11 @@ class BrowserManager { screen: DESKTOP_BROWSER_VIEWPORT } }) - context.once('close', () => { + context.once('close', async () => { this.activeBrowsers.delete(browser) - void browser.close().catch(() => {}) + if (browser.isConnected()) { + await this.closeBrowserWithTimeout(browser).catch(() => {}) + } }) await context.addInitScript(() => { @@ -263,7 +226,7 @@ class BrowserManager { return { context: context as unknown as BrowserContext, fingerprint } } catch (error) { this.activeBrowsers.delete(browser) - await browser.close().catch(() => {}) + await this.closeBrowserWithTimeout(browser).catch(() => {}) throw error } } @@ -271,7 +234,7 @@ class BrowserManager { async closeAll(): Promise { const browsers = [...this.activeBrowsers] this.activeBrowsers.clear() - await Promise.allSettled(browsers.map(browser => browser.close())) + await Promise.allSettled(browsers.map(browser => this.closeBrowserWithTimeout(browser))) } private formatProxyServer(proxy: AccountProxy): string { diff --git a/src/automation/PageController.ts b/src/automation/PageController.ts index c76d4b71..866041a7 100644 --- a/src/automation/PageController.ts +++ b/src/automation/PageController.ts @@ -1,5 +1,5 @@ import type { AxiosRequestConfig } from 'axios' -import type { Browser, BrowserContext, Cookie, Page } from 'patchright' +import type { BrowserContext, Cookie, Page } from 'patchright' import type { StorageOrigin } from '../helpers/ConfigLoader' import { saveSessionData, saveStorageState } from '../helpers/ConfigLoader' @@ -798,10 +798,7 @@ export default class PageController { } async closeBrowser(context: BrowserContext, email: string) { - let parentBrowser: Browser | null = null - try { - parentBrowser = context.browser() const cookies = await context.cookies() // Save cookies @@ -865,19 +862,9 @@ export default class PageController { await this.bot.utils.wait(2000) await this.closeWithTimeout(() => context.close(), 'context') - const browserToClose = parentBrowser - if (browserToClose?.isConnected()) { - await this.closeWithTimeout(() => browserToClose.close(), 'browser') - } this.bot.logger.info(this.bot.isMobile, 'CLOSE-BROWSER', 'Browser closed cleanly!') } catch (error) { - const browserToClose = parentBrowser - await Promise.allSettled([ - this.closeWithTimeout(() => context.close(), 'context').catch(() => {}), - browserToClose?.isConnected() - ? this.closeWithTimeout(() => browserToClose.close(), 'browser').catch(() => {}) - : Promise.resolve() - ]) + await this.closeWithTimeout(() => context.close(), 'context').catch(() => {}) this.bot.logger.error( this.bot.isMobile, 'CLOSE-BROWSER', diff --git a/tests/browser-runtime.test.js b/tests/browser-runtime.test.js index 8fa608fa..fc750783 100644 --- a/tests/browser-runtime.test.js +++ b/tests/browser-runtime.test.js @@ -14,6 +14,10 @@ test('browser runtime requires Patchright Chromium instead of silently using sys test('browser processes are tracked and closed during shutdown paths', () => { assert.match(browserManager, /activeBrowsers = new Set/) + assert.match(browserManager, /BROWSER_CLOSE_TIMEOUT_MS = 10_000/) + assert.match(browserManager, /closeBrowserWithTimeout\(browser\)/) + assert.match(browserManager, /context\.once\('close', async \(\) =>/) assert.match(browserManager, /async closeAll\(\)/) + assert.match(browserManager, /browsers\.map\(browser => this\.closeBrowserWithTimeout\(browser\)\)/) assert.match(main, /await rewardsBot\.closeAllBrowsers\(\)/) })