UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

169 lines (144 loc) 5.48 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import EventEmitter from 'events'; import log from 'lighthouse-logger'; import {LighthouseError} from '../lib/lh-error.js'; // Controls how long to wait for a response after sending a DevTools protocol command. const DEFAULT_PROTOCOL_TIMEOUT = 30000; const PPTR_BUFFER = 50; /** * Puppeteer timeouts must fit into an int32 and the maximum timeout for `setTimeout` is a *signed* * int32. However, this also needs to account for the puppeteer buffer we add to the timeout later. * * So this is defined as the max *signed* int32 minus PPTR_BUFFER. * * In human terms, this timeout is ~25 days which is as good as infinity for all practical purposes. */ const MAX_TIMEOUT = 2147483647 - PPTR_BUFFER; /** @typedef {LH.Protocol.StrictEventEmitterClass<LH.CrdpEvents>} CrdpEventMessageEmitter */ const CrdpEventEmitter = /** @type {CrdpEventMessageEmitter} */ (EventEmitter); /** @implements {LH.Gatherer.ProtocolSession} */ class ProtocolSession extends CrdpEventEmitter { /** * @param {LH.Puppeteer.CDPSession} cdpSession */ constructor(cdpSession) { super(); this._cdpSession = cdpSession; /** @type {LH.Crdp.Target.TargetInfo|undefined} */ this._targetInfo = undefined; /** @type {number|undefined} */ this._nextProtocolTimeout = undefined; this._handleProtocolEvent = this._handleProtocolEvent.bind(this); // @ts-expect-error Puppeteer expects the handler params to be type `unknown` this._cdpSession.on('*', this._handleProtocolEvent); // If the target crashes, we can't continue gathering. // FWIW, if the target unexpectedly detaches (eg the user closed the tab), pptr will // catch that and reject in this._cdpSession.send, which is caught by us. /** @param {Error} _ */ let rej = _ => {}; // Poor man's Promise.withResolvers() this._targetCrashedPromise = /** @type {Promise<never>} */ ( new Promise((_, theRej) => rej = theRej)); this.on('Inspector.targetCrashed', async () => { log.error('TargetManager', 'Inspector.targetCrashed'); // Manually detach so no more CDP traffic is attempted. // Don't await, else our rejection will be a 'Target closed' protocol error on cross-talk // CDP calls. void this.dispose(); rej(new LighthouseError(LighthouseError.errors.TARGET_CRASHED)); }); } id() { return this._cdpSession.id(); } /** * Re-emit protocol events from the underlying CDPSession. * @template {keyof LH.CrdpEvents} E * @param {E} method * @param {LH.CrdpEvents[E]} params */ _handleProtocolEvent(method, ...params) { this.emit(method, ...params); } /** @param {LH.Crdp.Target.TargetInfo} targetInfo */ setTargetInfo(targetInfo) { this._targetInfo = targetInfo; } /** * @return {boolean} */ hasNextProtocolTimeout() { return this._nextProtocolTimeout !== undefined; } /** * @return {number} */ getNextProtocolTimeout() { return this._nextProtocolTimeout || DEFAULT_PROTOCOL_TIMEOUT; } /** * @param {number} ms */ setNextProtocolTimeout(ms) { if (ms > MAX_TIMEOUT) ms = MAX_TIMEOUT; this._nextProtocolTimeout = ms; } /** * @template {keyof LH.CrdpCommands} C * @param {C} method * @param {LH.CrdpCommands[C]['paramsType']} params * @return {Promise<LH.CrdpCommands[C]['returnType']>} */ sendCommand(method, ...params) { const timeoutMs = this.getNextProtocolTimeout(); this._nextProtocolTimeout = undefined; /** @type {NodeJS.Timeout|undefined} */ let timeout; const timeoutPromise = new Promise((resolve, reject) => { // Unexpected setTimeout invocation to preserve the error stack. https://github.com/GoogleChrome/lighthouse/issues/13332 // eslint-disable-next-line max-len timeout = setTimeout(reject, timeoutMs, new LighthouseError(LighthouseError.errors.PROTOCOL_TIMEOUT, { protocolMethod: method, })); }); const resultPromise = this._cdpSession.send(method, ...params, { // Add 50ms to the Puppeteer timeout to ensure the Lighthouse timeout finishes first. timeout: timeoutMs + PPTR_BUFFER, }).catch((error) => { log.formatProtocol('method <= browser ERR', {method}, 'error'); throw LighthouseError.fromProtocolMessage(method, error); }); const resultWithTimeoutPromise = Promise.race([resultPromise, timeoutPromise, this._targetCrashedPromise]); return resultWithTimeoutPromise.finally(() => { if (timeout) clearTimeout(timeout); }); } /** * Send and if there's an error response, do not reject. * @template {keyof LH.CrdpCommands} C * @param {C} method * @param {LH.CrdpCommands[C]['paramsType']} params * @return {Promise<void>} */ sendCommandAndIgnore(method, ...params) { return this.sendCommand(method, ...params) .catch(e => log.verbose('session', method, e.message)).then(_ => void 0); } /** * Disposes of a session so that it can no longer talk to Chrome. * @return {Promise<void>} */ async dispose() { // @ts-expect-error Puppeteer expects the handler params to be type `unknown` this._cdpSession.off('*', this._handleProtocolEvent); await this._cdpSession.detach().catch(e => log.verbose('session', 'detach failed', e.message)); } onCrashPromise() { return this._targetCrashedPromise; } } export {ProtocolSession};