chore: migrate waitForEvent to Progress (#2483)

Drive-by: remove/simplify some helper code.
This commit is contained in:
Dmitry Gozman 2020-06-05 14:14:19 -07:00 committed by GitHub
parent fb058ffe0d
commit 300099734c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 102 deletions

View File

@ -104,8 +104,12 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise; return event === Events.BrowserContext.Close ? super._abortPromiseForEvent(event) : this._closePromise;
} }
protected _computeDeadline(options?: types.TimeoutOptions): number { protected _getLogger(): InnerLogger {
return this._timeoutSettings.computeDeadline(options); return this._logger;
}
protected _getTimeoutSettings(): TimeoutSettings {
return this._timeoutSettings;
} }
_browserClosed() { _browserClosed() {

View File

@ -15,23 +15,43 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { helper } from './helper'; import { helper, RegisteredListener } from './helper';
import { TimeoutOptions } from './types'; import { ProgressController } from './progress';
import { InnerLogger } from './logger';
import { TimeoutSettings } from './timeoutSettings';
export class ExtendedEventEmitter extends EventEmitter { export abstract class ExtendedEventEmitter extends EventEmitter {
protected _abortPromiseForEvent(event: string) { protected _abortPromiseForEvent(event: string) {
return new Promise<Error>(() => void 0); return new Promise<Error>(() => void 0);
} }
protected abstract _getLogger(): InnerLogger;
protected abstract _getTimeoutSettings(): TimeoutSettings;
protected _computeDeadline(options?: TimeoutOptions): number { async waitForEvent(event: string, optionsOrPredicate: Function | { predicate?: Function, timeout?: number } = {}): Promise<any> {
throw new Error('unimplemented'); const options = typeof optionsOrPredicate === 'function' ? { predicate: optionsOrPredicate } : optionsOrPredicate;
} const { predicate = () => true } = options;
async waitForEvent(event: string, optionsOrPredicate: Function|{ predicate?: Function, timeout?: number } = {}): Promise<any> { const progressController = new ProgressController(options, this._getLogger(), this._getTimeoutSettings());
const deadline = this._computeDeadline(typeof optionsOrPredicate === 'function' ? undefined : optionsOrPredicate); this._abortPromiseForEvent(event).then(error => progressController.abort(error));
const {
predicate = () => true, return progressController.run(async progress => {
} = typeof optionsOrPredicate === 'function' ? {predicate: optionsOrPredicate} : optionsOrPredicate; const listeners: RegisteredListener[] = [];
return helper.waitForEvent(this, event, predicate, deadline, this._abortPromiseForEvent(event)); const promise = new Promise((resolve, reject) => {
listeners.push(helper.addEventListener(this, event, eventArg => {
try {
if (!predicate(eventArg))
return;
resolve(eventArg);
} catch (e) {
reject(e);
}
}));
});
progress.cleanupWhenAborted(() => helper.removeEventListeners(listeners));
const result = await promise;
helper.removeEventListeners(listeners);
return result;
});
} }
} }

View File

@ -19,9 +19,7 @@ import * as crypto from 'crypto';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import * as fs from 'fs'; import * as fs from 'fs';
import * as util from 'util'; import * as util from 'util';
import { TimeoutError } from './errors';
import * as types from './types'; import * as types from './types';
import { ChildProcess, execSync } from 'child_process';
export type RegisteredListener = { export type RegisteredListener = {
emitter: EventEmitter; emitter: EventEmitter;
@ -124,59 +122,6 @@ class Helper {
return typeof obj === 'boolean' || obj instanceof Boolean; return typeof obj === 'boolean' || obj instanceof Boolean;
} }
static async waitForEvent(
emitter: EventEmitter,
eventName: (string | symbol),
predicate: Function,
deadline: number,
abortPromise: Promise<Error>): Promise<any> {
let resolveCallback: (event: any) => void = () => {};
let rejectCallback: (error: any) => void = () => {};
const promise = new Promise((resolve, reject) => {
resolveCallback = resolve;
rejectCallback = reject;
});
// Add listener.
const listener = Helper.addEventListener(emitter, eventName, event => {
try {
if (!predicate(event))
return;
resolveCallback(event);
} catch (e) {
rejectCallback(e);
}
});
// Reject upon timeout.
const eventTimeout = setTimeout(() => {
rejectCallback(new TimeoutError(`Timeout exceeded while waiting for ${String(eventName)}`));
}, helper.timeUntilDeadline(deadline));
// Reject upon abort.
abortPromise.then(rejectCallback);
try {
return await promise;
} finally {
Helper.removeEventListeners([listener]);
clearTimeout(eventTimeout);
}
}
static async waitWithDeadline<T>(promise: Promise<T>, taskName: string, deadline: number, debugName: string): Promise<T> {
let reject: (error: Error) => void;
const timeoutError = new TimeoutError(`Waiting for ${taskName} failed: timeout exceeded. Re-run with the DEBUG=${debugName} env variable to see the debug log.`);
const timeoutPromise = new Promise<T>((resolve, x) => reject = x);
const timeoutTimer = setTimeout(() => reject(timeoutError), helper.timeUntilDeadline(deadline));
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timeoutTimer)
clearTimeout(timeoutTimer);
}
}
static globToRegex(glob: string): RegExp { static globToRegex(glob: string): RegExp {
const tokens = ['^']; const tokens = ['^'];
let inGroup; let inGroup;
@ -321,31 +266,10 @@ class Helper {
return seconds * 1000 + (nanoseconds / 1000000 | 0); return seconds * 1000 + (nanoseconds / 1000000 | 0);
} }
static isPastDeadline(deadline: number) {
return deadline !== Number.MAX_SAFE_INTEGER && this.monotonicTime() >= deadline;
}
static timeUntilDeadline(deadline: number): number { static timeUntilDeadline(deadline: number): number {
return Math.min(deadline - this.monotonicTime(), 2147483647); // 2^31-1 safe setTimeout in Node. return Math.min(deadline - this.monotonicTime(), 2147483647); // 2^31-1 safe setTimeout in Node.
} }
static optionsWithUpdatedTimeout<T extends types.TimeoutOptions>(options: T | undefined, deadline: number): T {
return { ...(options || {}) as T, timeout: this.timeUntilDeadline(deadline) };
}
static killProcess(proc: ChildProcess) {
if (proc.pid && !proc.killed) {
try {
if (process.platform === 'win32')
execSync(`taskkill /pid ${proc.pid} /T /F`);
else
process.kill(-proc.pid, 'SIGKILL');
} catch (e) {
// the process might have already stopped
}
}
}
static getViewportSizeFromWindowFeatures(features: string[]): types.Size | null { static getViewportSizeFromWindowFeatures(features: string[]): types.Size | null {
const widthString = features.find(f => f.startsWith('width=')); const widthString = features.find(f => f.startsWith('width='));
const heightString = features.find(f => f.startsWith('height=')); const heightString = features.find(f => f.startsWith('height='));

View File

@ -140,8 +140,12 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
return this._disconnectedPromise; return this._disconnectedPromise;
} }
protected _computeDeadline(options?: types.TimeoutOptions): number { protected _getLogger(): InnerLogger {
return this._timeoutSettings.computeDeadline(options); return this;
}
protected _getTimeoutSettings(): TimeoutSettings {
return this._timeoutSettings;
} }
_didClose() { _didClose() {
@ -314,21 +318,21 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
} }
async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise<network.Request> { async waitForRequest(urlOrPredicate: string | RegExp | ((r: network.Request) => boolean), options: types.TimeoutOptions = {}): Promise<network.Request> {
const deadline = this._timeoutSettings.computeDeadline(options); const predicate = (request: network.Request) => {
return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => {
if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate))
return helper.urlMatches(request.url(), urlOrPredicate); return helper.urlMatches(request.url(), urlOrPredicate);
return urlOrPredicate(request); return urlOrPredicate(request);
}, deadline, this._disconnectedPromise); };
return this.waitForEvent(Events.Page.Request, { predicate, timeout: options.timeout });
} }
async waitForResponse(urlOrPredicate: string | RegExp | ((r: network.Response) => boolean), options: types.TimeoutOptions = {}): Promise<network.Response> { async waitForResponse(urlOrPredicate: string | RegExp | ((r: network.Response) => boolean), options: types.TimeoutOptions = {}): Promise<network.Response> {
const deadline = this._timeoutSettings.computeDeadline(options); const predicate = (response: network.Response) => {
return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => {
if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate))
return helper.urlMatches(response.url(), urlOrPredicate); return helper.urlMatches(response.url(), urlOrPredicate);
return urlOrPredicate(response); return urlOrPredicate(response);
}, deadline, this._disconnectedPromise); };
return this.waitForEvent(Events.Page.Response, { predicate, timeout: options.timeout });
} }
async goBack(options?: types.NavigateOptions): Promise<network.Response | null> { async goBack(options?: types.NavigateOptions): Promise<network.Response | null> {

View File

@ -83,10 +83,16 @@ export class BrowserServer extends EventEmitter {
} }
async _closeOrKill(deadline: number): Promise<void> { async _closeOrKill(deadline: number): Promise<void> {
let timer: NodeJS.Timer;
try { try {
await helper.waitWithDeadline(this.close(), '', deadline, ''); // The error message is ignored. await Promise.race([
this.close(),
new Promise((resolve, reject) => timer = setTimeout(reject, helper.timeUntilDeadline(deadline))),
]);
} catch (ignored) { } catch (ignored) {
await this.kill(); // Make sure to await actual process exit. await this.kill().catch(ignored => {}); // Make sure to await actual process exit.
} finally {
clearTimeout(timer!);
} }
} }
} }

View File

@ -153,8 +153,12 @@ export class ElectronApplication extends ExtendedEventEmitter {
return this._nodeElectronHandle!.evaluateHandle(pageFunction, arg); return this._nodeElectronHandle!.evaluateHandle(pageFunction, arg);
} }
protected _computeDeadline(options?: types.TimeoutOptions): number { protected _getLogger(): InnerLogger {
return this._timeoutSettings.computeDeadline(options); return this._logger;
}
protected _getTimeoutSettings(): TimeoutSettings {
return this._timeoutSettings;
} }
} }