2019-11-18 18:18:28 -08:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
* Modifications copyright ( c ) Microsoft Corporation .
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2019-12-05 14:48:39 -08:00
2020-04-01 14:42:47 -07:00
import * as crypto from 'crypto' ;
import { EventEmitter } from 'events' ;
import * as fs from 'fs' ;
import * as util from 'util' ;
2019-12-12 20:54:40 -08:00
import { TimeoutError } from './errors' ;
2020-03-03 16:09:32 -08:00
import * as types from './types' ;
2019-11-18 18:18:28 -08:00
export type RegisteredListener = {
2020-04-01 14:42:47 -07:00
emitter : EventEmitter ;
2019-11-18 18:18:28 -08:00
eventName : ( string | symbol ) ;
2019-12-09 15:41:20 -07:00
handler : ( . . . args : any [ ] ) = > void ;
2019-11-18 18:18:28 -08:00
} ;
2020-04-01 14:42:47 -07:00
export type Listener = ( . . . args : any [ ] ) = > void ;
2019-11-18 18:18:28 -08:00
class Helper {
static evaluationString ( fun : Function | string , . . . args : any [ ] ) : string {
if ( Helper . isString ( fun ) ) {
2020-03-20 15:08:17 -07:00
assert ( args . length === 0 || ( args . length === 1 && args [ 0 ] === undefined ) , 'Cannot evaluate a string with arguments' ) ;
2020-02-07 13:38:50 -08:00
return fun ;
2019-11-18 18:18:28 -08:00
}
return ` ( ${ fun } )( ${ args . map ( serializeArgument ) . join ( ',' ) } ) ` ;
function serializeArgument ( arg : any ) : string {
if ( Object . is ( arg , undefined ) )
return 'undefined' ;
return JSON . stringify ( arg ) ;
}
}
2020-03-20 15:08:17 -07:00
static async evaluationScript ( fun : Function | string | { path? : string , content? : string } , arg? : any , addSourceUrl : boolean = true ) : Promise < string > {
2020-02-27 17:42:14 -08:00
if ( ! helper . isString ( fun ) && typeof fun !== 'function' ) {
if ( fun . content !== undefined ) {
fun = fun . content ;
} else if ( fun . path !== undefined ) {
2020-04-01 14:42:47 -07:00
let contents = await util . promisify ( fs . readFile ) ( fun . path , 'utf8' ) ;
2020-02-28 15:34:07 -08:00
if ( addSourceUrl )
contents += '//# sourceURL=' + fun . path . replace ( /\n/g , '' ) ;
2020-02-27 17:42:14 -08:00
fun = contents ;
} else {
throw new Error ( 'Either path or content property must be present' ) ;
}
}
2020-03-20 15:08:17 -07:00
return helper . evaluationString ( fun , arg ) ;
2020-02-27 17:42:14 -08:00
}
2020-01-16 17:48:38 -08:00
static installApiHooks ( className : string , classType : any ) {
2019-11-18 18:18:28 -08:00
for ( const methodName of Reflect . ownKeys ( classType . prototype ) ) {
const method = Reflect . get ( classType . prototype , methodName ) ;
2020-01-16 17:48:38 -08:00
if ( methodName === 'constructor' || typeof methodName !== 'string' || methodName . startsWith ( '_' ) || typeof method !== 'function' )
continue ;
const isAsync = method . constructor . name === 'AsyncFunction' ;
2020-04-20 07:52:26 -07:00
if ( ! isAsync )
2019-11-18 18:18:28 -08:00
continue ;
2020-01-13 13:33:25 -08:00
Reflect . set ( classType . prototype , methodName , function ( this : any , . . . args : any [ ] ) {
2020-02-25 07:09:27 -08:00
const syncStack : any = { } ;
Error . captureStackTrace ( syncStack ) ;
2020-01-13 13:33:25 -08:00
return method . call ( this , . . . args ) . catch ( ( e : any ) = > {
2019-11-18 18:18:28 -08:00
const stack = syncStack . stack . substring ( syncStack . stack . indexOf ( '\n' ) + 1 ) ;
const clientStack = stack . substring ( stack . indexOf ( '\n' ) ) ;
if ( e instanceof Error && e . stack && ! e . stack . includes ( clientStack ) )
e . stack += '\n -- ASYNC --\n' + stack ;
throw e ;
} ) ;
} ) ;
}
}
static addEventListener (
2020-04-01 14:42:47 -07:00
emitter : EventEmitter ,
2019-11-18 18:18:28 -08:00
eventName : ( string | symbol ) ,
2019-12-09 15:41:20 -07:00
handler : ( . . . args : any [ ] ) = > void ) : RegisteredListener {
2019-11-18 18:18:28 -08:00
emitter . on ( eventName , handler ) ;
return { emitter , eventName , handler } ;
}
static removeEventListeners ( listeners : Array < {
2020-04-01 14:42:47 -07:00
emitter : EventEmitter ;
2019-11-18 18:18:28 -08:00
eventName : ( string | symbol ) ;
2019-12-09 15:41:20 -07:00
handler : ( . . . args : any [ ] ) = > void ;
2019-11-18 18:18:28 -08:00
} > ) {
for ( const listener of listeners )
listener . emitter . removeListener ( listener . eventName , listener . handler ) ;
listeners . splice ( 0 , listeners . length ) ;
}
static isString ( obj : any ) : obj is string {
return typeof obj === 'string' || obj instanceof String ;
}
static isNumber ( obj : any ) : obj is number {
return typeof obj === 'number' || obj instanceof Number ;
}
2020-02-25 10:32:17 +08:00
static isRegExp ( obj : any ) : obj is RegExp {
return obj instanceof RegExp || Object . prototype . toString . call ( obj ) === '[object RegExp]' ;
}
2020-02-22 06:16:28 -08:00
static isObject ( obj : any ) : obj is NonNullable < object > {
return typeof obj === 'object' && obj !== null ;
}
static isBoolean ( obj : any ) : obj is boolean {
return typeof obj === 'boolean' || obj instanceof Boolean ;
}
2019-11-18 18:18:28 -08:00
static async waitForEvent (
2020-04-01 14:42:47 -07:00
emitter : EventEmitter ,
2019-11-18 18:18:28 -08:00
eventName : ( string | symbol ) ,
predicate : Function ,
2020-04-07 14:35:34 -07:00
deadline : number ,
2019-11-18 18:18:28 -08:00
abortPromise : Promise < Error > ) : Promise < any > {
2020-01-13 13:33:25 -08:00
let resolveCallback : ( event : any ) = > void = ( ) = > { } ;
let rejectCallback : ( error : any ) = > void = ( ) = > { } ;
2019-11-18 18:18:28 -08:00
const promise = new Promise ( ( resolve , reject ) = > {
resolveCallback = resolve ;
rejectCallback = reject ;
} ) ;
const listener = Helper . addEventListener ( emitter , eventName , event = > {
2019-12-17 14:00:39 -08:00
try {
if ( ! predicate ( event ) )
return ;
resolveCallback ( event ) ;
} catch ( e ) {
rejectCallback ( e ) ;
}
2019-11-18 18:18:28 -08:00
} ) ;
2020-04-07 14:35:34 -07:00
const eventTimeout = setTimeout ( ( ) = > {
rejectCallback ( new TimeoutError ( ` Timeout exceeded while waiting for ${ String ( eventName ) } ` ) ) ;
} , helper . timeUntilDeadline ( deadline ) ) ;
2019-11-18 18:18:28 -08:00
function cleanup() {
Helper . removeEventListeners ( [ listener ] ) ;
clearTimeout ( eventTimeout ) ;
}
2020-04-20 11:37:02 -07:00
return await Promise . race ( [ promise , abortPromise ] ) . then ( r = > {
2019-11-18 18:18:28 -08:00
cleanup ( ) ;
return r ;
} , e = > {
cleanup ( ) ;
throw e ;
} ) ;
}
static async waitWithTimeout < T > ( promise : Promise < T > , taskName : string , timeout : number ) : Promise < T > {
2020-04-07 14:35:34 -07:00
return this . waitWithDeadline ( promise , taskName , helper . monotonicTime ( ) + timeout ) ;
}
static async waitWithDeadline < T > ( promise : Promise < T > , taskName : string , deadline : number ) : Promise < T > {
2019-11-18 18:18:28 -08:00
let reject : ( error : Error ) = > void ;
2020-05-04 21:44:33 -07:00
const timeoutError = new TimeoutError ( ` Waiting for ${ taskName } failed: timeout exceeded. Re-run with the DEBUG=pw:input env variable to see the debug log. ` ) ;
2019-11-18 18:18:28 -08:00
const timeoutPromise = new Promise < T > ( ( resolve , x ) = > reject = x ) ;
2020-04-07 14:35:34 -07:00
const timeoutTimer = setTimeout ( ( ) = > reject ( timeoutError ) , helper . timeUntilDeadline ( deadline ) ) ;
2019-11-18 18:18:28 -08:00
try {
return await Promise . race ( [ promise , timeoutPromise ] ) ;
} finally {
if ( timeoutTimer )
clearTimeout ( timeoutTimer ) ;
}
}
2020-02-03 14:23:24 -08:00
static globToRegex ( glob : string ) : RegExp {
const tokens = [ '^' ] ;
let inGroup ;
for ( let i = 0 ; i < glob . length ; ++ i ) {
const c = glob [ i ] ;
if ( escapeGlobChars . has ( c ) ) {
tokens . push ( '\\' + c ) ;
continue ;
}
if ( c === '*' ) {
const beforeDeep = glob [ i - 1 ] ;
let starCount = 1 ;
while ( glob [ i + 1 ] === '*' ) {
starCount ++ ;
i ++ ;
}
const afterDeep = glob [ i + 1 ] ;
const isDeep = starCount > 1 &&
( beforeDeep === '/' || beforeDeep === undefined ) &&
( afterDeep === '/' || afterDeep === undefined ) ;
if ( isDeep ) {
tokens . push ( '((?:[^/]*(?:\/|$))*)' ) ;
i ++ ;
} else {
tokens . push ( '([^/]*)' ) ;
}
continue ;
}
switch ( c ) {
case '?' :
tokens . push ( '.' ) ;
break ;
case '{' :
inGroup = true ;
tokens . push ( '(' ) ;
break ;
case '}' :
inGroup = false ;
tokens . push ( ')' ) ;
break ;
case ',' :
if ( inGroup ) {
tokens . push ( '|' ) ;
break ;
}
tokens . push ( '\\' + c ) ;
break ;
default :
tokens . push ( c ) ;
}
}
tokens . push ( '$' ) ;
return new RegExp ( tokens . join ( '' ) ) ;
}
2020-02-04 19:39:52 -08:00
static completeUserURL ( urlString : string ) : string {
if ( urlString . startsWith ( 'localhost' ) || urlString . startsWith ( '127.0.0.1' ) )
urlString = 'http://' + urlString ;
return urlString ;
}
2020-02-25 07:09:27 -08:00
static trimMiddle ( string : string , maxLength : number ) {
if ( string . length <= maxLength )
return string ;
const leftHalf = maxLength >> 1 ;
const rightHalf = maxLength - leftHalf - 1 ;
return string . substr ( 0 , leftHalf ) + '\u2026' + string . substr ( this . length - rightHalf , rightHalf ) ;
}
2020-03-03 16:09:32 -08:00
static enclosingIntRect ( rect : types.Rect ) : types . Rect {
const x = Math . floor ( rect . x + 1 e - 3 ) ;
const y = Math . floor ( rect . y + 1 e - 3 ) ;
const x2 = Math . ceil ( rect . x + rect . width - 1 e - 3 ) ;
const y2 = Math . ceil ( rect . y + rect . height - 1 e - 3 ) ;
return { x , y , width : x2 - x , height : y2 - y } ;
}
static enclosingIntSize ( size : types.Size ) : types . Size {
return { width : Math.floor ( size . width + 1 e - 3 ) , height : Math.floor ( size . height + 1 e - 3 ) } ;
}
2020-03-16 14:39:44 -07:00
static urlMatches ( urlString : string , match : types.URLMatch | undefined ) : boolean {
if ( match === undefined || match === '' )
return true ;
if ( helper . isString ( match ) )
match = helper . globToRegex ( match ) ;
if ( helper . isRegExp ( match ) )
return match . test ( urlString ) ;
if ( typeof match === 'string' && match === urlString )
return true ;
const url = new URL ( urlString ) ;
if ( typeof match === 'string' )
return url . pathname === match ;
assert ( typeof match === 'function' , 'url parameter should be string, RegExp or function' ) ;
return match ( url ) ;
}
2020-04-01 14:42:47 -07:00
// See https://joel.tools/microtasks/
static makeWaitForNextTask() {
if ( parseInt ( process . versions . node , 10 ) >= 11 )
return setImmediate ;
// Unlike Node 11, Node 10 and less have a bug with Task and MicroTask execution order:
// - https://github.com/nodejs/node/issues/22257
//
// So we can't simply run setImmediate to dispatch code in a following task.
// However, we can run setImmediate from-inside setImmediate to make sure we're getting
// in the following task.
let spinning = false ;
const callbacks : ( ( ) = > void ) [ ] = [ ] ;
const loop = ( ) = > {
const callback = callbacks . shift ( ) ;
if ( ! callback ) {
spinning = false ;
return ;
}
setImmediate ( loop ) ;
// Make sure to call callback() as the last thing since it's
// untrusted code that might throw.
callback ( ) ;
} ;
return ( callback : ( ) = > void ) = > {
callbacks . push ( callback ) ;
if ( ! spinning ) {
spinning = true ;
setImmediate ( loop ) ;
}
} ;
}
static guid ( ) : string {
return crypto . randomBytes ( 16 ) . toString ( 'hex' ) ;
}
2020-04-07 14:35:34 -07:00
static monotonicTime ( ) : number {
const [ seconds , nanoseconds ] = process . hrtime ( ) ;
return seconds * 1000 + ( nanoseconds / 1000000 | 0 ) ;
}
2020-04-18 18:29:31 -07:00
static isPastDeadline ( deadline : number ) {
return deadline !== Number . MAX_SAFE_INTEGER && this . monotonicTime ( ) >= deadline ;
}
2020-04-07 14:35:34 -07:00
static timeUntilDeadline ( deadline : number ) : number {
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 ) } ;
}
2019-11-18 18:18:28 -08:00
}
2020-02-05 16:53:36 -08:00
export function assert ( value : any , message? : string ) : asserts value {
2019-11-18 18:18:28 -08:00
if ( ! value )
throw new Error ( message ) ;
}
2020-05-08 10:37:54 -07:00
let _isUnderTest = false ;
export function setUnderTest() {
_isUnderTest = true ;
}
export function isUnderTest ( ) : boolean {
return _isUnderTest ;
}
export function debugAssert ( value : any , message? : string ) : asserts value {
if ( _isUnderTest && ! value )
throw new Error ( message ) ;
}
2020-04-29 18:35:04 -07:00
export function assertMaxArguments ( count : number , max : number ) : asserts count {
assert ( count <= max , 'Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.' ) ;
}
2020-04-28 10:37:23 -07:00
export function getFromENV ( name : string ) {
let value = process . env [ name ] ;
value = value || process . env [ ` npm_config_ ${ name . toLowerCase ( ) } ` ] ;
value = value || process . env [ ` npm_package_config_ ${ name . toLowerCase ( ) } ` ] ;
return value ;
}
export function logPolitely ( toBeLogged : string ) {
const logLevel = process . env . npm_config_loglevel ;
const logLevelDisplay = [ 'silent' , 'error' , 'warn' ] . indexOf ( logLevel || '' ) > - 1 ;
if ( ! logLevelDisplay )
console . log ( toBeLogged ) ; // eslint-disable-line no-console
}
2020-02-03 14:23:24 -08:00
const escapeGlobChars = new Set ( [ '/' , '$' , '^' , '+' , '.' , '(' , ')' , '=' , '!' , '|' ] ) ;
2019-11-18 18:18:28 -08:00
export const helper = Helper ;