1+ /*---------------------------------------------------------
2+ * Copyright (C) Microsoft Corporation. All rights reserved.
3+ *--------------------------------------------------------*/
4+
5+ import { logger } from 'vscode-debugadapter' ;
6+ import { ISetBreakpointResult } from '../debugAdapterInterfaces' ;
7+
8+ import Crdp from '../../crdp/crdp' ;
9+ import { ChromeDebugAdapter } from './chromeDebugAdapter' ;
10+
11+ import * as path from 'path' ;
12+
13+ export class BreakOnLoadHelper {
14+
15+ public userBreakpointOnLine1Col1 : boolean = false ;
16+ private _instrumentationBreakpointSet : boolean = false ;
17+
18+ // Break on load: Store some mapping between the requested file names, the regex for the file, and the chrome breakpoint id to perform lookup operations efficiently
19+ private _stopOnEntryBreakpointIdToRequestedFileName = new Map < string , [ string , Set < string > ] > ( ) ;
20+ private _stopOnEntryRequestedFileNameToBreakpointId = new Map < string , string > ( ) ;
21+ private _stopOnEntryRegexToBreakpointId = new Map < string , string > ( ) ;
22+
23+ private _chromeDebugAdapter : ChromeDebugAdapter ;
24+
25+ public constructor ( chromeDebugAdapter : ChromeDebugAdapter ) {
26+ this . _chromeDebugAdapter = chromeDebugAdapter ;
27+ }
28+
29+ public get stopOnEntryRequestedFileNameToBreakpointId ( ) : Map < string , string > {
30+ return this . _stopOnEntryRequestedFileNameToBreakpointId ;
31+ }
32+
33+ public get stopOnEntryBreakpointIdToRequestedFileName ( ) : Map < string , [ string , Set < string > ] > {
34+ return this . _stopOnEntryBreakpointIdToRequestedFileName ;
35+ }
36+
37+ public get instrumentationBreakpointSet ( ) : boolean {
38+ return this . _instrumentationBreakpointSet ;
39+ }
40+
41+ /**
42+ * Checks and resolves the pending breakpoints of a script. If any breakpoints were resolved returns true, else false.
43+ * Used when break on load active, either through Chrome's Instrumentation Breakpoint API or the regex approach
44+ */
45+ public async resolvePendingBreakpointsOfPausedScript ( scriptId : string ) : Promise < boolean > {
46+ const pausedScriptUrl = this . _chromeDebugAdapter . scriptsById . get ( scriptId ) . url ;
47+ const mappedUrl = await this . _chromeDebugAdapter . pathTransformer . scriptParsed ( pausedScriptUrl ) ;
48+
49+ const pendingBreakpoints = this . _chromeDebugAdapter . pendingBreakpointsByUrl . get ( mappedUrl ) ;
50+ // If the file has unbound breakpoints, resolve them and return true
51+ if ( pendingBreakpoints !== undefined ) {
52+ await this . _chromeDebugAdapter . resolvePendingBreakpoint ( pendingBreakpoints ) ;
53+ return true ;
54+ } else {
55+ // If no pending breakpoints, return false
56+ return false ;
57+ }
58+ }
59+
60+ /**
61+ * Returns whether we should continue on hitting a stopOnEntry breakpoint
62+ * Only used when using regex approach for break on load
63+ */
64+ private async shouldContinueOnStopOnEntryBreakpoint ( scriptId : string ) : Promise < boolean > {
65+ // If the file has no unbound breakpoints or none of the resolved breakpoints are at (1,1), we should continue after hitting the stopOnEntry breakpoint
66+ let shouldContinue = true ;
67+ let anyPendingBreakpointsResolved = await this . resolvePendingBreakpointsOfPausedScript ( scriptId ) ;
68+
69+ // If there were any pending breakpoints resolved and any of them was at (1,1) we shouldn't continue
70+ if ( anyPendingBreakpointsResolved && this . userBreakpointOnLine1Col1 ) {
71+ // Here we need to store this information per file, but since we can safely assume that scriptParsed would immediately be followed by onPaused event
72+ // for the breakonload files, this implementation should be fine
73+ this . userBreakpointOnLine1Col1 = false ;
74+ shouldContinue = false ;
75+ }
76+
77+ return shouldContinue ;
78+ }
79+
80+ /**
81+ * Handles a script with a stop on entry breakpoint and returns whether we should continue or not on hitting that breakpoint
82+ * Only used when using regex approach for break on load
83+ */
84+ public async handleStopOnEntryBreakpointAndContinue ( notification : Crdp . Debugger . PausedEvent ) : Promise < boolean > {
85+ const hitBreakpoints = notification . hitBreakpoints ;
86+ let allStopOnEntryBreakpoints = true ;
87+
88+ // If there is a breakpoint which is not a stopOnEntry breakpoint, we appear as if we hit that one
89+ // This is particularly done for cases when we end up with a user breakpoint and a stopOnEntry breakpoint on the same line
90+ hitBreakpoints . forEach ( bp => {
91+ if ( ! this . _stopOnEntryBreakpointIdToRequestedFileName . has ( bp ) ) {
92+ notification . hitBreakpoints = [ bp ] ;
93+ allStopOnEntryBreakpoints = false ;
94+ }
95+ } ) ;
96+
97+ // If all the breakpoints on this point are stopOnEntry breakpoints
98+ // This will be true in cases where it's a single breakpoint and it's a stopOnEntry breakpoint
99+ // This can also be true when we have multiple breakpoints and all of them are stopOnEntry breakpoints, for example in cases like index.js and index.bin.js
100+ // Suppose user puts breakpoints in both index.js and index.bin.js files, when the setBreakpoints function is called for index.js it will set a stopOnEntry
101+ // breakpoint on index.* files which will also match index.bin.js. Now when setBreakpoints is called for index.bin.js it will again put a stopOnEntry breakpoint
102+ // in itself. So when the file is actually loaded, we would have 2 stopOnEntry breakpoints */
103+
104+ if ( allStopOnEntryBreakpoints ) {
105+ const pausedScriptId = notification . callFrames [ 0 ] . location . scriptId ;
106+ let shouldContinue = await this . shouldContinueOnStopOnEntryBreakpoint ( pausedScriptId ) ;
107+ if ( shouldContinue ) {
108+ return true ;
109+ }
110+ }
111+ return false ;
112+ }
113+
114+ /**
115+ * Adds a stopOnEntry breakpoint for the given script url
116+ * Only used when using regex approach for break on load
117+ */
118+ public async addStopOnEntryBreakpoint ( url : string ) : Promise < ISetBreakpointResult [ ] > {
119+ let responsePs : ISetBreakpointResult [ ] ;
120+ // Check if file already has a stop on entry breakpoint
121+ if ( ! this . _stopOnEntryRequestedFileNameToBreakpointId . has ( url ) ) {
122+
123+ // Generate regex we need for the file
124+ const urlRegex = this . getUrlRegexForBreakOnLoad ( url ) ;
125+
126+ // Check if we already have a breakpoint for this regexp since two different files like script.ts and script.js may have the same regexp
127+ let breakpointId : string ;
128+ breakpointId = this . _stopOnEntryRegexToBreakpointId . get ( urlRegex ) ;
129+
130+ // If breakpointId is undefined it means the breakpoint doesn't exist yet so we add it
131+ if ( breakpointId === undefined ) {
132+ let result ;
133+ try {
134+ result = await this . setStopOnEntryBreakpoint ( urlRegex ) ;
135+ } catch ( e ) {
136+ logger . log ( `Exception occured while trying to set stop on entry breakpoint ${ e . message } .` ) ;
137+ }
138+ if ( result ) {
139+ breakpointId = result . breakpointId ;
140+ this . _stopOnEntryRegexToBreakpointId . set ( urlRegex , breakpointId ) ;
141+ } else {
142+ logger . log ( `BreakpointId was null when trying to set on urlregex ${ urlRegex } . This normally happens if the breakpoint already exists.` ) ;
143+ }
144+ responsePs = [ result ] ;
145+ } else {
146+ responsePs = [ ] ;
147+ }
148+
149+ // Store the new breakpointId and the file name in the right mappings
150+ this . _stopOnEntryRequestedFileNameToBreakpointId . set ( url , breakpointId ) ;
151+
152+ let regexAndFileNames = this . _stopOnEntryBreakpointIdToRequestedFileName . get ( breakpointId ) ;
153+
154+ // If there already exists an entry for the breakpoint Id, we add this file to the list of file mappings
155+ if ( regexAndFileNames !== undefined ) {
156+ regexAndFileNames [ 1 ] . add ( url ) ;
157+ } else { // else create an entry for this breakpoint id
158+ const fileSet = new Set < string > ( ) ;
159+ fileSet . add ( url ) ;
160+ this . _stopOnEntryBreakpointIdToRequestedFileName . set ( breakpointId , [ urlRegex , fileSet ] ) ;
161+ }
162+ } else {
163+ responsePs = [ ] ;
164+ }
165+ return Promise . all ( responsePs ) ;
166+ }
167+
168+ /**
169+ * Tells Chrome to set instrumentation breakpoint to stop on all the scripts before execution
170+ * Only used when using instrument approach for break on load
171+ */
172+ public async setInstrumentationBreakpoint ( ) : Promise < void > {
173+ this . _chromeDebugAdapter . chrome . DOMDebugger . setInstrumentationBreakpoint ( { eventName : "scriptFirstStatement" } ) ;
174+ this . _instrumentationBreakpointSet = true ;
175+ }
176+
177+ // Sets a breakpoint on (0,0) for the files matching the given regex
178+ private async setStopOnEntryBreakpoint ( urlRegex : string ) : Promise < Crdp . Debugger . SetBreakpointByUrlResponse > {
179+ let result = await this . _chromeDebugAdapter . chrome . Debugger . setBreakpointByUrl ( { urlRegex, lineNumber : 0 , columnNumber : 0 , condition : '' } ) ;
180+ return result ;
181+ }
182+
183+ /* Constructs the regex for files to enable break on load
184+ For example, for a file index.js the regex will match urls containing index.js, index.ts, abc/index.ts, index.bin.js etc
185+ It won't match index100.js, indexabc.ts etc */
186+ private getUrlRegexForBreakOnLoad ( url : string ) : string {
187+ const fileNameWithoutFullPath = path . parse ( url ) . base ;
188+ const fileNameWithoutExtension = path . parse ( fileNameWithoutFullPath ) . name ;
189+ const escapedFileName = fileNameWithoutExtension . replace ( / [ - \/ \\ ^ $ * + ? . ( ) | [ \] { } ] / g, '\\$&' ) ;
190+
191+ return ".*[\\\\\\/]" + escapedFileName + "([^A-z^0-9].*)?$" ;
192+ }
193+ }
0 commit comments