webshot-factory
Version:
screenshots at scale based on headless chrome
152 lines (135 loc) • 4.84 kB
text/typescript
import * as _ from 'lodash';
import * as puppeteer from 'puppeteer';
import * as Logger from 'log4js';
import * as P from 'bluebird';
import { config } from 'bluebird';
let _logger = Logger.getLogger("shot-worker");
const DEBUG_PORT_OFFSET = 9201;
export interface WorkerConfig {
callbackName?: string;
warmerUrl?: string;
width?: number;
height?: number;
timeout?: number;
}
export class ShotWorker {
public creationTime: number;
private debugPort: number;
private browser;
private page;
private timeout: number = 60000;
private shotCallback: (err, buffer: Buffer) => void = _.noop;
private isBusy: boolean = true;
public config: WorkerConfig;
constructor(public id: number) {
}
public static async create(idx: number, config: WorkerConfig) {
let worker = new ShotWorker(idx);
worker.config = config;
await worker.init(idx, config.callbackName, config.warmerUrl, config.width, config.height, config.timeout);
return worker;
}
public takeShot(url: string): P<Buffer> {
if (this.isBusy) {
_logger.error('Worker is busy doing work');
return P.reject('Worker is already busy');
}
let start = (new Date()).valueOf();
this.isBusy = true;
_logger.debug(`screenshot url #${this.id}: ${url}`);
return new P<Buffer>(async (resolve, reject) => {
this.shotCallback = async (err, buffer: Buffer) => {
if (err) {
return reject(err);
}
resolve(buffer);
};
this.page.goto(url, {
waitUntil: 'networkidle2'
}).then(async () => {
if(!this.config.callbackName) {
let buffer = await this.page.screenshot({
fullPage: true
});
this.shotCallback(null, buffer);
}
}).then(null, (err) => {
this.shotCallback(err, null);
});
})
.timeout(this.timeout)
.finally(() => {
_logger.debug(`Worker #${this.id}: Screenshot Complete.`)
this.isBusy = false
});
}
public reload() {
return this.page.reload();
}
public exit() {
this.browser && this.browser.close();
}
public getStatus() {
return {
id: this.id,
browser: this.browser,
debugPort: this.debugPort,
isBusy: this.isBusy
}
}
private async init(idx: number = 0,
callbackName: string = '',
warmerUrl: string = '',
width: number = 800,
height: number = 600,
timeout: number = 60000,
chromeExecutablePath: string = '') {
this.debugPort = DEBUG_PORT_OFFSET + idx;
this.timeout = timeout;
let start = (new Date()).valueOf();
let options: any = {
ignoreHTTPSErrors: true,
headless: true,
args: [
'--ignore-certificate-errors',
'--enable-precise-memory-info',
`--remote-debugging-port=${this.debugPort}`],
userDataDir: '/tmp/chrome'
};
if (chromeExecutablePath) {
options.executablePath = chromeExecutablePath
}
try {
this.browser = await puppeteer.launch(options);
_logger.debug('puppeteer launched');
} catch (error) {
_logger.error("error launching chrome from puppeteer", error);
}
this.page = await this.browser.newPage();
this.page.on('console', (...args) => _logger.debug(`PAGE LOG Worker #${idx}:`, ...args));
this.page.on('response', r => _logger.debug(`Worker #${idx}: ${r.status} ${r.url}`));
this.page.setViewport({
width: width,
height: height
});
// Define a window.onCustomEvent function on the page.
if(callbackName) {
await this.page.exposeFunction(callbackName, e => {
_logger.debug('Callback called from browser with', e);
return this.page.screenshot({
fullPage: true
}).then((buffer: Buffer) => {
this.shotCallback(null, buffer);
}, (err) => {
this.shotCallback(err, null);
});
});
}
if (warmerUrl) {
await this.page.goto(warmerUrl, { waitUntil: 'networkidle2' });
}
_logger.info(`Worker ${idx} ready`);
this.creationTime = (new Date()).valueOf() - start;
this.isBusy = false;
}
}