import { logger } from 'vscode-debugadapter' ; import { ISetBreakpointResult , BreakOnLoadStrategy } from '../debugAdapterInterfaces' ; import { Protocol as Crdp } from 'devtools-protocol' ; import { ChromeDebugAdapter } from './chromeDebugAdapter' ; import * as ChromeUtils from './chromeUtils' ; import * as assert from 'assert' ; import { InternalSourceBreakpoint } from './internalSourceBreakpoint' ; import { utils , Version } from '..' ; export interface UrlRegexAndFileSet { urlRegex : string ; fileSet : Set < string > ; } export class BreakOnLoadHelper { private _doesDOMInstrumentationRecieveExtraEvent = false ; private _instrumentationBreakpointSet = false ; private _stopOnEntryBreakpointIdToRequestedFileName = new Map < string , UrlRegexAndFileSet > ( ) ; private _stopOnEntryRequestedFileNameToBreakpointId = new Map < string , string > ( ) ; private _stopOnEntryRegexToBreakpointId = new Map < string , string > ( ) ; private _chromeDebugAdapter : ChromeDebugAdapter ; private _breakOnLoadStrategy : BreakOnLoadStrategy ; public constructor ( chromeDebugAdapter , breakOnLoadStrategy ) { this . validateStrategy ( breakOnLoadStrategy ) ; this . _chromeDebugAdapter = chromeDebugAdapter ; this . _breakOnLoadStrategy = breakOnLoadStrategy ; } validateStrategy ( breakOnLoadStrategy ) { if ( breakOnLoadStrategy !== 'regex' && breakOnLoadStrategy !== 'instrument' ) { throw new Error ( 'Invalid breakOnLoadStrategy: ' + breakOnLoadStrategy ) ; } } public get stopOnEntryRequestedFileNameToBreakpointId ( ) : Map < string , string > { return this . _stopOnEntryRequestedFileNameToBreakpointId ; } public get stopOnEntryBreakpointIdToRequestedFileName ( ) : Map < string , UrlRegexAndFileSet > { return this . _stopOnEntryBreakpointIdToRequestedFileName ; } private get instrumentationBreakpointSet ( ) : boolean { return this . _instrumentationBreakpointSet ; } private getScriptUrlFromId ( scriptId ) { return utils . canonicalizeUrl ( this . _chromeDebugAdapter . scriptsById . get ( scriptId ) . url ) ; } public async setBrowserVersion ( version ) { this . _doesDOMInstrumentationRecieveExtraEvent = ! version . isAtLeastVersion ( 69 , 0 ) ; } /**
     * Handles the onpaused event.
     * Checks if the event is caused by a stopOnEntry breakpoint of using the regex approach, or the paused event due to the Chrome's instrument approach
     * Returns whether we should continue or not on this paused event
     */ public async handleOnPaused ( notification ) { if ( notification . hitBreakpoints && notification . hitBreakpoints . length ) { if ( this . _breakOnLoadStrategy === 'regex' ) { let shouldContinue = await this . handleStopOnEntryBreakpointAndContinue ( notification ) ; return shouldContinue ; } } else if ( this . isInstrumentationPause ( notification ) ) { const pausedScriptId = notification . callFrames [ 0 ] . location . scriptId ; await this . _chromeDebugAdapter . getBreakpointsResolvedDefer ( pausedScriptId ) . promise ; logger . log ( 'BreakOnLoadHelper: Finished waiting for breakpoints to get resolved.' ) ; let shouldContinue = this . _doesDOMInstrumentationRecieveExtraEvent || await this . handleStopOnEntryBreakpointAndContinue ( notification ) ; return shouldContinue ; } return false ; } private isInstrumentationPause ( notification ) { return ( notification . reason === 'EventListener' && notification . data . eventName === 'instrumentation:scriptFirstStatement' ) || ( notification . reason === 'ambiguous' && Array . isArray ( notification . data . reasons ) && notification . data . reasons . every ( r => r . reason === 'EventListener' && r . auxData . eventName === 'instrumentation:scriptFirstStatement' ) ) ; } /**
     * Returns whether we should continue on hitting a stopOnEntry breakpoint
     * Only used when using regex approach for break on load
     */ private async shouldContinueOnStopOnEntryBreakpoint ( pausedLocation ) { let shouldContinue = true ; await this . _chromeDebugAdapter . getBreakpointsResolvedDefer ( pausedLocation . scriptId ) . promise ; const pausedScriptUrl = this . getScriptUrlFromId ( pausedLocation . scriptId ) ; const committedBps = this . _chromeDebugAdapter . committedBreakpointsByUrl . get ( pausedScriptUrl ) || [ ] ; const anyBreakpointsAtPausedLocation = committedBps . filter ( bp => bp . actualLocation && bp . actualLocation . lineNumber === pausedLocation . lineNumber && bp . actualLocation . columnNumber === pausedLocation . columnNumber ) . length > 0 ; if ( anyBreakpointsAtPausedLocation ) { shouldContinue = false ; } return shouldContinue ; } /**
     * Handles a script with a stop on entry breakpoint and returns whether we should continue or not on hitting that breakpoint
     * Only used when using regex approach for break on load
     */ private async handleStopOnEntryBreakpointAndContinue ( notification ) { const hitBreakpoints = notification . hitBreakpoints ; let allStopOnEntryBreakpoints = true ; const pausedScriptId = notification . callFrames [ 0 ] . location . scriptId ; const pausedScriptUrl = this . _chromeDebugAdapter . scriptsById . get ( pausedScriptId ) . url ; const mappedUrl = await this . _chromeDebugAdapter . pathTransformer . scriptParsed ( pausedScriptUrl ) ; for ( let bp of hitBreakpoints ) { let regexAndFileNames = this . _stopOnEntryBreakpointIdToRequestedFileName . get ( bp ) ; if ( ! regexAndFileNames ) { notification . hitBreakpoints = [ bp ] ; allStopOnEntryBreakpoints = false ; } else { const normalizedMappedUrl = utils . canonicalizeUrl ( mappedUrl ) ; if ( regexAndFileNames . fileSet . has ( normalizedMappedUrl ) ) { regexAndFileNames . fileSet . delete ( normalizedMappedUrl ) ; assert ( this . _stopOnEntryRequestedFileNameToBreakpointId . delete ( normalizedMappedUrl ) , `Expected to delete break-on-load information associated with url: ${ normalizedMappedUrl }` ) ; if ( regexAndFileNames . fileSet . size === 0 ) { logger . log ( `Stop on entry breakpoint hit for last remaining file. Removing: ${ bp } created for: ${ normalizedMappedUrl }` ) ; await this . removeBreakpointById ( bp ) ; assert ( this . _stopOnEntryRegexToBreakpointId . delete ( regexAndFileNames . urlRegex ) , `Expected to delete break-on-load information associated with regexp: ${ regexAndFileNames . urlRegex }` ) ; } else { logger . log ( `Stop on entry breakpoint hit but still has remaining files. Keeping: ${ bp } that was hit for: ${ normalizedMappedUrl } because it's still needed for: ${ Array . from ( regexAndFileNames . fileSet . entries ( ) ) . join ( ', ' ) }` ) ; } } } } if ( allStopOnEntryBreakpoints ) { const pausedLocation = notification . callFrames [ 0 ] . location ; let shouldContinue = await this . shouldContinueOnStopOnEntryBreakpoint ( pausedLocation ) ; if ( shouldContinue ) { return true ; } } return false ; } /**
     * Adds a stopOnEntry breakpoint for the given script url
     * Only used when using regex approach for break on load
     */ private async addStopOnEntryBreakpoint ( url ) { let responsePs ; if ( ! this . _stopOnEntryRequestedFileNameToBreakpointId . has ( url ) ) { const normalizedUrl = utils . canonicalizeUrl ( url ) ; const urlRegex = ChromeUtils . getUrlRegexForBreakOnLoad ( normalizedUrl ) ; let breakpointId ; breakpointId = this . _stopOnEntryRegexToBreakpointId . get ( urlRegex ) ; if ( breakpointId === undefined ) { let result ; try { result = await this . setStopOnEntryBreakpoint ( urlRegex ) ; } catch ( e ) { logger . log ( `Exception occured while trying to set stop on entry breakpoint ${ e . message }.` ) ; } if ( result ) { breakpointId = result . breakpointId ; this . _stopOnEntryRegexToBreakpointId . set ( urlRegex , breakpointId ) ; } else { logger . log ( `BreakpointId was null when trying to set on urlregex ${ urlRegex }. This normally happens if the breakpoint already exists.` ) ; } responsePs = [ result ] ; } else { responsePs = [ ] ; } this . _stopOnEntryRequestedFileNameToBreakpointId . set ( normalizedUrl , breakpointId ) ; let regexAndFileNames = this . _stopOnEntryBreakpointIdToRequestedFileName . get ( breakpointId ) ; if ( regexAndFileNames !== undefined ) { regexAndFileNames . fileSet . add ( normalizedUrl ) ; } else { const fileSet = new Set < string > ( ) ; fileSet . add ( normalizedUrl ) ; this . _stopOnEntryBreakpointIdToRequestedFileName . set ( breakpointId , { urlRegex , fileSet } ) ; } } else { responsePs = [ ] ; } return Promise . all ( responsePs ) ; } /**
     * Handles the AddBreakpoints request when break on load is active
     * Takes the action based on the strategy
     */ public async handleAddBreakpoints ( url , breakpoints ) { if ( this . _breakOnLoadStrategy === 'regex' ) { await this . addStopOnEntryBreakpoint ( url ) ; } else if ( this . _breakOnLoadStrategy === 'instrument' ) { if ( ! this . instrumentationBreakpointSet ) { await this . setInstrumentationBreakpoint ( ) ; } } return breakpoints . map ( breakpoint => { return { } ; } ) ; } /**
     * Tells Chrome to set instrumentation breakpoint to stop on all the scripts before execution
     * Only used when using instrument approach for break on load
     */ private async setInstrumentationBreakpoint ( ) { await this . _chromeDebugAdapter . chrome . DOMDebugger . setInstrumentationBreakpoint ( { eventName : 'scriptFirstStatement' } ) ; this . _instrumentationBreakpointSet = true ; } private async setStopOnEntryBreakpoint ( urlRegex ) { let result = await this . _chromeDebugAdapter . chrome . Debugger . setBreakpointByUrl ( { urlRegex , lineNumber : 0 , columnNumber : 0 } ) ; return result ; } private async removeBreakpointById ( breakpointId ) { return await this . _chromeDebugAdapter . chrome . Debugger . removeBreakpoint ( { breakpointId : breakpointId } ) ; } /**
     * Checks if we need to call resolvePendingBPs on scriptParsed event
     * If break on load is active and we are using the regex approach, only call the resolvePendingBreakpoint function for files where we do not
     * set break on load breakpoints. For those files, it is called from onPaused function.
     * For the default Chrome's API approach, we don't need to call resolvePendingBPs from inside scriptParsed
     */ public shouldResolvePendingBPs ( mappedUrl ) { if ( this . _breakOnLoadStrategy === 'regex' && ! this . stopOnEntryRequestedFileNameToBreakpointId . has ( mappedUrl ) ) { return true ; } return false ; } }