Skip to content

Commit

Permalink
Add retry to puppeteer functions to address #164
Browse files Browse the repository at this point in the history
  • Loading branch information
claabs committed Dec 28, 2021
1 parent 74ab645 commit b681613
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 14 deletions.
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"docs": "typedoc --out docs src/common/config/classes.ts && cp test/test.html docs"
},
"dependencies": {
"cancelable-promise": "^4.2.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"dotenv": "^8.2.0",
Expand Down
50 changes: 49 additions & 1 deletion src/common/puppeteer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import puppeteer from 'puppeteer-extra';
import { Page, Protocol } from 'puppeteer';
import { Page, Protocol, Browser } from 'puppeteer';
import PortalPlugin, { WebPortalConnectionConfig } from 'puppeteer-extra-plugin-portal';
import objectAssignDeep from 'object-assign-deep';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { Logger } from 'pino';
import { cancelable } from 'cancelable-promise';
import { ToughCookieFileStore } from './request';
import { config } from './config';

Expand Down Expand Up @@ -105,3 +107,49 @@ export const launchArgs: Parameters<typeof puppeteer.launch>[0] = {
// '--remote-debugging-address=0.0.0.0', // Change devtools url to localhost
],
};

/**
* This is a hacky solution to retry a function if it doesn't return within a timeout.
* It leaves behind a danging chromium process, but it should get cleaned up when this parent node
* process exits if the container is using tini.
*/
const retryFunction = async <T>(
f: () => Promise<T>,
L: Logger,
outputName: string,
attempts = 0
): Promise<T> => {
const TIMEOUT = 30 * 1000;
const MAX_ATTEMPTS = 30;
const newPageCancelable = cancelable(f());
const res = await Promise.race([
newPageCancelable,
// eslint-disable-next-line no-promise-executor-return
new Promise((resolve) => setTimeout(resolve, TIMEOUT)).then(() => 'timeout'),
]);
if (typeof res !== 'string') {
return res;
}
newPageCancelable.cancel();
if (attempts > MAX_ATTEMPTS)
throw new Error(`Could not do ${outputName} after ${MAX_ATTEMPTS} attempts.`);
L.debug(
{ attempts, MAX_ATTEMPTS },
`${outputName} did not work after ${TIMEOUT}ms. Trying again.`
);
return retryFunction(f, L, outputName, attempts + 1);
};

/**
* Create a new page within a wrapper that will retry if it hangs for 30 seconds
*/
export const safeNewPage = (browser: Browser, L: Logger): Promise<Page> => {
return retryFunction(() => browser.newPage(), L, 'new page');
};

/**
* Launcha new browser within a wrapper that will retry if it hangs for 30 seconds
*/
export const safeLaunchBrowser = (L: Logger): Promise<Browser> => {
return retryFunction(() => puppeteer.launch(launchArgs), L, 'browser launch');
};
6 changes: 3 additions & 3 deletions src/notify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from './common/config';
import L from './common/logger';
import { NotificationReason } from './interfaces/notification-reason';
import puppeteer, { getDevtoolsUrl, launchArgs } from './common/puppeteer';
import { getDevtoolsUrl, safeLaunchBrowser, safeNewPage } from './common/puppeteer';
import { getLocaltunnelUrl } from './common/localtunnel';

export async function sendNotification(
Expand Down Expand Up @@ -61,8 +61,8 @@ export async function sendNotification(

export async function testNotifiers(): Promise<void> {
L.info('Testing all configured notifiers');
const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();
const browser = await safeLaunchBrowser(L);
const page = await safeNewPage(browser, L);
L.trace(getDevtoolsUrl(page));
await page.goto('https://claabs.github.io/epicgames-freegames-node/test.html');
let url = await page.openPortal();
Expand Down
6 changes: 3 additions & 3 deletions src/puppet/hcaptcha.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs-extra';
import { ElementHandle, Protocol } from 'puppeteer';
import path from 'path';
import puppeteer, { getDevtoolsUrl, launchArgs } from '../common/puppeteer';
import { getDevtoolsUrl, safeLaunchBrowser, safeNewPage } from '../common/puppeteer';
import { config, CONFIG_DIR } from '../common/config';
import L from '../common/logger';

Expand Down Expand Up @@ -44,8 +44,8 @@ export const getHcaptchaCookies = async (): Promise<Protocol.Network.Cookie[]> =
if (!cookieData) {
try {
L.debug('Setting hCaptcha accessibility cookies');
browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();
browser = await safeLaunchBrowser(L);
const page = await safeNewPage(browser, L);

L.trace(getDevtoolsUrl(page));
L.trace(`Navigating to ${hcaptchaAccessibilityUrl}`);
Expand Down
9 changes: 5 additions & 4 deletions src/puppet/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { Logger } from 'pino';
import { Protocol, ElementHandle, Page } from 'puppeteer';
import path from 'path';
import logger from '../common/logger';
import puppeteer, {
import {
getDevtoolsUrl,
launchArgs,
safeLaunchBrowser,
safeNewPage,
toughCookieFileStoreToPuppeteerCookie,
} from '../common/puppeteer';
import { getCookiesRaw, setPuppeteerCookies } from '../common/request';
Expand Down Expand Up @@ -70,8 +71,8 @@ export default class PuppetLogin {
const userCookies = await getCookiesRaw(this.email);
const puppeteerCookies = toughCookieFileStoreToPuppeteerCookie(userCookies);
this.L.debug('Logging in with puppeteer');
const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();
const browser = await safeLaunchBrowser(this.L);
const page = await safeNewPage(browser, this.L);
try {
this.L.trace(getDevtoolsUrl(page));
const cdpClient = await page.target().createCDPSession();
Expand Down
7 changes: 5 additions & 2 deletions src/puppet/purchase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import logger from '../common/logger';
import puppeteer, {
getDevtoolsUrl,
launchArgs,
safeLaunchBrowser,
safeNewPage,
toughCookieFileStoreToPuppeteerCookie,
} from '../common/puppeteer';
import { getCookiesRaw, setPuppeteerCookies } from '../common/request';
Expand Down Expand Up @@ -83,6 +85,7 @@ export default class PuppetPurchase {
if (startTime.getTime() + timeout <= new Date().getTime()) {
throw new Error(`Timeout after ${timeout}ms: ${err.message}`);
}
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, poll));
return waitForPurchaseButton(startTime);
}
Expand Down Expand Up @@ -140,8 +143,8 @@ export default class PuppetPurchase {
const userCookies = await getCookiesRaw(this.email);
const puppeteerCookies = toughCookieFileStoreToPuppeteerCookie(userCookies);
this.L.debug('Purchasing with puppeteer (short)');
const browser = await puppeteer.launch(launchArgs);
const page = await browser.newPage();
const browser = await safeLaunchBrowser(this.L);
const page = await safeNewPage(browser, this.L);
this.L.trace(getDevtoolsUrl(page));
const cdpClient = await page.target().createCDPSession();
try {
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"removeComments": true, /* Do not emit comments to output. */
Expand Down

0 comments on commit b681613

Please sign in to comment.