import { DebugProtocol } from 'vscode-debugprotocol' ; import { BreakpointEvent , logger } from 'vscode-debugadapter' ; import { ISetBreakpointsArgs , ISetBreakpointsResponseBody , ISetBreakpointResult } from '../debugAdapterInterfaces' ; import * as ChromeUtils from './chromeUtils' ; import { Protocol as Crdp } from 'devtools-protocol' ; import { ReasonType } from './stoppedEvent' ; import { InternalSourceBreakpoint } from './internalSourceBreakpoint' ; import { ScriptContainer } from './scripts' ; import { ChromeDebugAdapter } from '..' ; import { IPendingBreakpoint , BreakpointSetResult } from './chromeDebugAdapter' ; import * as utils from '../utils' ; import * as path from 'path' ; import * as nls from 'vscode-nls' ; import { ChromeConnection } from './chromeConnection' ; let localize = nls . loadMessageBundle ( ) ; export interface IHitConditionBreakpoint { numHits : number ; shouldPause : ( numHits : number ) => boolean ; } /**
 * Encapsulates all the logic surrounding breakpoints (e.g. set, unset, hit count breakpoints, etc.)
 */ export class Breakpoints { private static SET_BREAKPOINTS_TIMEOUT = 5000 ; private static HITCONDITION_MATCHER = /^(>|>=|=|<|<=|%)?\s*([0-9]+)$/ ; private _breakpointIdHandles : utils . ReverseHandles < Crdp . Debugger . BreakpointId > ; private _nextUnboundBreakpointId = 0 ; private _pendingBreakpointsByUrl : Map < string , IPendingBreakpoint > ; private _hitConditionBreakpointsById : Map < Crdp . Debugger . BreakpointId , IHitConditionBreakpoint > ; private _committedBreakpointsByUrl = new Map < string , ISetBreakpointResult [ ] > ( ) ; private _setBreakpointsRequestQ : Promise < any > = Promise . resolve ( ) ; public get breakpointsQueueDrained ( ) : Promise < void > { return this . _setBreakpointsRequestQ ; } public get committedBreakpointsByUrl ( ) : Map < string , ISetBreakpointResult [ ] > { return this . _committedBreakpointsByUrl ; } private getValueFromCommittedBreakpointsByUrl ( url ) { let canonicalizedUrl = utils . canonicalizeUrl ( url ) ; return this . _committedBreakpointsByUrl . get ( canonicalizedUrl ) ; } private setValueForCommittedBreakpointsByUrl ( url , value ) { let canonicalizedUrl = utils . canonicalizeUrl ( url ) ; this . _committedBreakpointsByUrl . set ( canonicalizedUrl , value ) ; } private get chrome ( ) { return this . _chromeConnection . api ; } constructor ( private readonly adapter , private readonly _chromeConnection ) { this . _breakpointIdHandles = new utils . ReverseHandles < Crdp . Debugger . BreakpointId > ( ) ; this . _pendingBreakpointsByUrl = new Map < string , IPendingBreakpoint > ( ) ; this . _hitConditionBreakpointsById = new Map < Crdp . Debugger . BreakpointId , IHitConditionBreakpoint > ( ) ; } reset ( ) { this . _committedBreakpointsByUrl = new Map < string , ISetBreakpointResult [ ] > ( ) ; this . _setBreakpointsRequestQ = Promise . resolve ( ) ; } param args param scripts param requestSeq param ids public setBreakpoints ( args , scripts , requestSeq , ids ? ) { if ( args . source . path ) { args . source . path = this . adapter . displayPathToRealPath ( args . source . path ) ; args . source . path = utils . canonicalizeUrl ( args . source . path ) ; } return this . validateBreakpointsPath ( args ) . then ( ( ) => { const originalArgs = args ; args = JSON . parse ( JSON . stringify ( args ) ) ; args = this . adapter . lineColTransformer . setBreakpoints ( args ) ; const sourceMapTransformerResponse = this . adapter . sourceMapTransformer . setBreakpoints ( args , requestSeq , ids ) ; if ( sourceMapTransformerResponse && sourceMapTransformerResponse . args ) { args = sourceMapTransformerResponse . args ; } if ( sourceMapTransformerResponse && sourceMapTransformerResponse . ids ) { ids = sourceMapTransformerResponse . ids ; } args . source = this . adapter . pathTransformer . setBreakpoints ( args . source ) ; let targetScriptUrl ; if ( args . source . sourceReference ) { const handle = scripts . getSource ( args . source . sourceReference ) ; if ( ( ! handle || ! handle . scriptId ) && args . source . path ) { targetScriptUrl = args . source . path ; } else { const targetScript = scripts . getScriptById ( handle . scriptId ) ; if ( targetScript ) { targetScriptUrl = targetScript . url ; } } } else if ( args . source . path ) { targetScriptUrl = args . source . path ; } if ( targetScriptUrl ) { const internalBPs = args . breakpoints . map ( bp => new InternalSourceBreakpoint ( bp ) ) ; const setBreakpointsPFailOnError = this . _setBreakpointsRequestQ . then ( ( ) => this . clearAllBreakpoints ( targetScriptUrl ) ) . then ( ( ) => this . addBreakpoints ( targetScriptUrl , internalBPs , scripts ) ) . then ( responses => ( { breakpoints : this . targetBreakpointResponsesToBreakpointSetResults ( targetScriptUrl , responses , internalBPs , ids ) } ) ) ; const setBreakpointsPTimeout = utils . promiseTimeout ( setBreakpointsPFailOnError , Breakpoints . SET_BREAKPOINTS_TIMEOUT , localize ( 'setBPTimedOut' , 'Set breakpoints request timed out' ) ) ; this . _setBreakpointsRequestQ = setBreakpointsPTimeout . catch ( e => { if ( e . message && e . message . indexOf ( 'timed out' ) >= 0 ) { logger . error ( e . stack ) ; } } ) ; return setBreakpointsPFailOnError . then ( setBpResultBody => { const body = { breakpoints : setBpResultBody . breakpoints . map ( setBpResult => setBpResult . breakpoint ) } ; if ( body . breakpoints . every ( bp => ! bp . verified ) ) { return this . unverifiedBpResponseForBreakpoints ( originalArgs , requestSeq , targetScriptUrl , body . breakpoints , localize ( 'bp.fail.unbound' , 'Breakpoint set but not yet bound' ) ) ; } body . breakpoints = this . adapter . sourceMapTransformer . setBreakpointsResponse ( body . breakpoints , true , requestSeq ) || body . breakpoints ; this . adapter . lineColTransformer . setBreakpointsResponse ( body ) ; return body ; } ) ; } else { return Promise . resolve ( this . unverifiedBpResponse ( args , requestSeq , undefined , localize ( 'bp.fail.noscript' , "Can't find script for breakpoint request" ) ) ) ; } } , e => this . unverifiedBpResponse ( args , requestSeq , undefined , e . message ) ) ; } protected validateBreakpointsPath ( args ) { if ( ! args . source . path || args . source . sourceReference ) return Promise . resolve ( ) ; if ( this . adapter . breakOnLoadActive ) { return Promise . resolve ( ) ; } return this . adapter . sourceMapTransformer . getGeneratedPathFromAuthoredPath ( args . source . path ) . then < void > ( mappedPath => { if ( ! mappedPath ) { return utils . errP ( localize ( 'validateBP.sourcemapFail' , 'Breakpoint ignored because generated code not found (source map problem?).' ) ) ; } const targetPath = this . adapter . pathTransformer . getTargetPathFromClientPath ( mappedPath ) ; if ( ! targetPath ) { return utils . errP ( localize ( 'validateBP.notFound' , 'Breakpoint ignored because target path not found' ) ) ; } return undefined ; } ) ; } /**
     * Makes the actual call to either Debugger.setBreakpoint or Debugger.setBreakpointByUrl, and returns the response.
     * Responses from setBreakpointByUrl are transformed to look like the response from setBreakpoint, so they can be
     * handled the same.
     */ protected async addBreakpoints ( url , breakpoints , scripts ) { let responsePs ; if ( ChromeUtils . isEvalScript ( url ) ) { const scriptId = utils . lstrip ( url , ChromeUtils . EVAL_NAME_PREFIX ) ; responsePs = breakpoints . map ( ( { line , column = 0 , condition } ) => this . chrome . Debugger . setBreakpoint ( { location : { scriptId , lineNumber : line , columnNumber : column } , condition } ) ) ; } else { const script = scripts . getScriptByUrl ( url ) ; if ( ! this . adapter . breakOnLoadActive || script ) { const urlRegex = utils . pathToRegex ( url ) ; responsePs = breakpoints . map ( ( { line , column = 0 , condition } ) => { return this . addOneBreakpointByUrl ( script && script . scriptId , urlRegex , line , column , condition ) ; } ) ; } else { if ( this . adapter . breakOnLoadActive ) { return await this . adapter . breakOnLoadHelper . handleAddBreakpoints ( url , breakpoints ) ; } } } return Promise . all ( responsePs ) ; } param scriptId param urlRegex param lineNumber param columnNumber param condition async addOneBreakpointByUrl ( scriptId , urlRegex , lineNumber , columnNumber , condition ) { let bpLocation = { lineNumber , columnNumber } ; if ( this . adapter . columnBreakpointsEnabled && scriptId ) { try { const possibleBpResponse = await this . chrome . Debugger . getPossibleBreakpoints ( { start : { scriptId , lineNumber , columnNumber : 0 } , end : { scriptId , lineNumber : lineNumber + 1 , columnNumber : 0 } , restrictToFunction : false } ) ; if ( possibleBpResponse . locations . length ) { const selectedLocation = ChromeUtils . selectBreakpointLocation ( lineNumber , columnNumber , possibleBpResponse . locations ) ; bpLocation = { lineNumber : selectedLocation . lineNumber , columnNumber : selectedLocation . columnNumber || 0 } ; } } catch ( e ) { } } let result ; try { result = await this . chrome . Debugger . setBreakpointByUrl ( { urlRegex , lineNumber : bpLocation . lineNumber , columnNumber : bpLocation . columnNumber , condition } ) ; } catch ( e ) { if ( e . message === 'Breakpoint at specified location already exists.' ) { return { actualLocation : { lineNumber : bpLocation . lineNumber , columnNumber : bpLocation . columnNumber , scriptId } } ; } else { throw e ; } } const locations = result . locations ; return < Crdp . Debugger . SetBreakpointResponse > { breakpointId : result . breakpointId , actualLocation : locations [ 0 ] && { lineNumber : locations [ 0 ] . lineNumber , columnNumber : locations [ 0 ] . columnNumber , scriptId } } ; } param args param scripts param requestSeq param ids public async getBreakpointsLocations ( args , scripts , requestSeq ) { if ( args . source . path ) { args . source . path = this . adapter . displayPathToRealPath ( args . source . path ) ; args . source . path = utils . canonicalizeUrl ( args . source . path ) ; } try { await this . validateBreakpointsPath ( args ) ; } catch ( e ) { logger . log ( 'breakpointsLocations failed: ' + e . message ) ; return { breakpoints : [ ] } ; } args = JSON . parse ( JSON . stringify ( args ) ) ; args . endLine = this . adapter . lineColTransformer . convertClientLineToDebugger ( typeof args . endLine === 'number' ? args . endLine : args . line + 1 ) ; args . endColumn = this . adapter . lineColTransformer . convertClientLineToDebugger ( args . endColumn || 1 ) ; args . line = this . adapter . lineColTransformer . convertClientLineToDebugger ( args . line ) ; args . column = this . adapter . lineColTransformer . convertClientColumnToDebugger ( args . column || 1 ) ; if ( args . source . path ) { const source1 = JSON . parse ( JSON . stringify ( args . source ) ) ; const startArgs = this . adapter . sourceMapTransformer . setBreakpoints ( { breakpoints : [ { line : args . line , column : args . column } ] , source : source1 } , requestSeq ) ; args . line = startArgs . args . breakpoints [ 0 ] . line ; args . column = startArgs . args . breakpoints [ 0 ] . column ; const endArgs = this . adapter . sourceMapTransformer . setBreakpoints ( { breakpoints : [ { line : args . endLine , column : args . endColumn } ] , source : args . source } , requestSeq ) ; args . endLine = endArgs . args . breakpoints [ 0 ] . line ; args . endColumn = endArgs . args . breakpoints [ 0 ] . column ; } args . source = this . adapter . pathTransformer . setBreakpoints ( args . source ) ; let targetScriptUrl ; if ( args . source . sourceReference ) { const handle = scripts . getSource ( args . source . sourceReference ) ; if ( ( ! handle || ! handle . scriptId ) && args . source . path ) { targetScriptUrl = args . source . path ; } else { const targetScript = scripts . getScriptById ( handle . scriptId ) ; if ( targetScript ) { targetScriptUrl = targetScript . url ; } } } else if ( args . source . path ) { targetScriptUrl = args . source . path ; } if ( targetScriptUrl ) { const script = scripts . getScriptByUrl ( targetScriptUrl ) ; if ( script ) { const end = typeof args . endLine === 'number' ? { scriptId : script . scriptId , lineNumber : args . endLine , columnNumber : args . endColumn || 0 } : { scriptId : script . scriptId , lineNumber : args . line + 1 , columnNumber : 0 } ; const possibleBpResponse = await this . chrome . Debugger . getPossibleBreakpoints ( { start : { scriptId : script . scriptId , lineNumber : args . line , columnNumber : args . column || 0 } , end , restrictToFunction : false } ) ; if ( possibleBpResponse . locations ) { let breakpoints = possibleBpResponse . locations . map ( loc => { return < DebugProtocol . Breakpoint > { line : loc . lineNumber , column : loc . columnNumber } ; } ) ; breakpoints = this . adapter . sourceMapTransformer . setBreakpointsResponse ( breakpoints , false , requestSeq ) ; const response = { breakpoints } ; this . adapter . lineColTransformer . setBreakpointsResponse ( response ) ; return response as DebugProtocol . BreakpointLocationsResponse [ 'body' ] ; } else { return { breakpoints : [ ] } ; } } } return null ; } param url param responses param requestBps param ids private targetBreakpointResponsesToBreakpointSetResults ( url , responses , requestBps , ids ? ) { const committedBps = responses . filter ( response => response && response . breakpointId ) ; this . setValueForCommittedBreakpointsByUrl ( url , committedBps ) ; return responses . map ( ( response , i ) => { if ( ! response ) { return { isSet : false , breakpoint : < DebugProtocol . Breakpoint > { verified : false } } ; } const responseBpId = response . breakpointId || this . generateNextUnboundBreakpointId ( ) ; let bpId ; if ( ids && ids [ i ] ) { bpId = ids [ i ] ; this . _breakpointIdHandles . set ( bpId , responseBpId ) ; } else { bpId = this . _breakpointIdHandles . lookup ( responseBpId ) || this . _breakpointIdHandles . create ( responseBpId ) ; } if ( ! response . actualLocation ) { return { isSet : response . breakpointId !== undefined , breakpoint : < DebugProtocol . Breakpoint > { id : bpId , verified : false } } ; } const thisBpRequest = requestBps [ i ] ; if ( thisBpRequest . hitCondition ) { if ( ! this . addHitConditionBreakpoint ( thisBpRequest , response ) ) { return { isSet : true , breakpoint : < DebugProtocol . Breakpoint > { id : bpId , message : localize ( 'invalidHitCondition' , 'Invalid hit condition: {0}' , thisBpRequest . hitCondition ) , verified : false } } ; } } return { isSet : true , breakpoint : < DebugProtocol . Breakpoint > { id : bpId , verified : true , line : response . actualLocation . lineNumber , column : response . actualLocation . columnNumber } } ; } ) ; } private addHitConditionBreakpoint ( requestBp , response ) { const result = Breakpoints . HITCONDITION_MATCHER . exec ( requestBp . hitCondition . trim ( ) ) ; if ( result && result . length >= 3 ) { let op = result [ 1 ] || '>=' ; if ( op === '=' ) op = '==' ; const value = result [ 2 ] ; const expr = op === '%' ? `return (numHits % ${ value }) === 0;` : `return numHits ${ op } ${ value };` ; const shouldPause = < any > new Function ( 'numHits' , expr ) ; this . _hitConditionBreakpointsById . set ( response . breakpointId , { numHits : 0 , shouldPause } ) ; return true ; } else { return false ; } } private clearAllBreakpoints ( url ) { url = utils . canonicalizeUrl ( url ) ; if ( ! this . _committedBreakpointsByUrl . has ( url ) ) { return Promise . resolve ( ) ; } return this . _committedBreakpointsByUrl . get ( url ) . reduce ( ( p , bp ) => { return p . then ( ( ) => this . chrome . Debugger . removeBreakpoint ( { breakpointId : bp . breakpointId } ) ) . then ( ( ) => { } ) ; } , Promise . resolve ( ) ) . then ( ( ) => { this . _committedBreakpointsByUrl . delete ( url ) ; } ) ; } public onBreakpointResolved ( params , scripts ) { const script = scripts . getScriptById ( params . location . scriptId ) ; const breakpointId = this . _breakpointIdHandles . lookup ( params . breakpointId ) ; if ( ! script || ! breakpointId ) { return ; } if ( this . adapter . breakOnLoadActive && this . adapter . breakOnLoadHelper . stopOnEntryBreakpointIdToRequestedFileName . has ( params . breakpointId ) ) { return ; } const committedBps = this . getValueFromCommittedBreakpointsByUrl ( script . url ) || [ ] ; if ( ! committedBps . find ( committedBp => committedBp . breakpointId === params . breakpointId ) ) { committedBps . push ( { breakpointId : params . breakpointId , actualLocation : params . location } ) ; } this . setValueForCommittedBreakpointsByUrl ( script . url , committedBps ) ; const bp = < DebugProtocol . Breakpoint > { id : breakpointId , verified : true , line : params . location . lineNumber , column : params . location . columnNumber } ; const scriptPath = utils . canonicalizeUrl ( this . adapter . pathTransformer . breakpointResolved ( bp , script . url ) ) ; if ( this . _pendingBreakpointsByUrl . has ( scriptPath ) ) { this . _pendingBreakpointsByUrl . delete ( scriptPath ) ; } this . adapter . sourceMapTransformer . breakpointResolved ( bp , scriptPath ) ; this . adapter . lineColTransformer . breakpointResolved ( bp ) ; this . adapter . session . sendEvent ( new BreakpointEvent ( 'changed' , bp ) ) ; } private generateNextUnboundBreakpointId ( ) { const unboundBreakpointUniquePrefix = '__::[vscode_chrome_debug_adapter_unbound_breakpoint]::' ; return `${ unboundBreakpointUniquePrefix }${ this . _nextUnboundBreakpointId ++ }` ; } private unverifiedBpResponse ( args , requestSeq , targetScriptUrl , message ? ) { const breakpoints = args . breakpoints . map ( bp => { return < DebugProtocol . Breakpoint > { verified : false , line : bp . line , column : bp . column , message , id : this . _breakpointIdHandles . create ( this . generateNextUnboundBreakpointId ( ) ) } ; } ) ; return this . unverifiedBpResponseForBreakpoints ( args , requestSeq , targetScriptUrl , breakpoints , message ) ; } private unverifiedBpResponseForBreakpoints ( args , requestSeq , targetScriptUrl , breakpoints , defaultMessage ? ) { breakpoints . forEach ( bp => { if ( ! bp . message ) { bp . message = defaultMessage ; } } ) ; if ( args . source . path ) { const ids = breakpoints . map ( bp => bp . id ) ; this . _pendingBreakpointsByUrl . set ( utils . canonicalizeUrl ( args . source . path ) , { args , ids , requestSeq , setWithPath : this . adapter . breakOnLoadActive ? '' : targetScriptUrl } ) ; } return { breakpoints } ; } public async handleScriptParsed ( script , scripts , mappedUrl , sources ) { if ( sources ) { const filteredSources = sources . filter ( source => source !== mappedUrl ) ; for ( const filteredSource of filteredSources ) { await this . resolvePendingBPs ( filteredSource , scripts ) ; } } if ( utils . canonicalizeUrl ( script . url ) === mappedUrl && this . _pendingBreakpointsByUrl . has ( mappedUrl ) && utils . canonicalizeUrl ( this . _pendingBreakpointsByUrl . get ( mappedUrl ) . setWithPath ) === utils . canonicalizeUrl ( mappedUrl ) ) { this . _pendingBreakpointsByUrl . delete ( mappedUrl ) ; } else { await this . resolvePendingBPs ( mappedUrl , scripts ) ; } } public async resolvePendingBPs ( source , scripts ) { source = source && utils . canonicalizeUrl ( source ) ; const pendingBP = this . _pendingBreakpointsByUrl . get ( source ) ; if ( pendingBP && ( ! pendingBP . setWithPath || utils . canonicalizeUrl ( pendingBP . setWithPath ) === source ) ) { logger . log ( `OnScriptParsed.resolvePendingBPs: Resolving pending breakpoints: ${ JSON . stringify ( pendingBP ) }` ) ; await this . resolvePendingBreakpoint ( pendingBP , scripts ) ; this . _pendingBreakpointsByUrl . delete ( source ) ; } else if ( source ) { const sourceFileName = path . basename ( source ) . toLowerCase ( ) ; if ( Array . from ( this . _pendingBreakpointsByUrl . keys ( ) ) . find ( key => key . toLowerCase ( ) . indexOf ( sourceFileName ) > - 1 ) ) { logger . log ( `OnScriptParsed.resolvePendingBPs: The following pending breakpoints won't be resolved: ${ JSON . stringify ( pendingBP ) } pendingBreakpointsByUrl = ${ JSON . stringify ( [ ... this . _pendingBreakpointsByUrl ] ) } source = ${ source }` ) ; } } } public resolvePendingBreakpoint ( pendingBP , scripts ) { return this . setBreakpoints ( pendingBP . args , scripts , pendingBP . requestSeq , pendingBP . ids ) . then ( response => { response . breakpoints . forEach ( ( bp , i ) => { bp . id = pendingBP . ids [ i ] ; this . adapter . session . sendEvent ( new BreakpointEvent ( 'changed' , bp ) ) ; } ) ; } ) ; } public handleHitCountBreakpoints ( expectingStopReason , hitBreakpoints ) { for ( let hitBp of hitBreakpoints ) { if ( this . _hitConditionBreakpointsById . has ( hitBp ) ) { const hitConditionBp = this . _hitConditionBreakpointsById . get ( hitBp ) ; hitConditionBp . numHits ++ ; if ( ! expectingStopReason && ! hitConditionBp . shouldPause ( hitConditionBp . numHits ) ) { this . chrome . Debugger . resume ( ) . catch ( ( ) => { } ) ; return { didPause : false } ; } } } return null ; } }