This repository has been archived by the owner on Oct 2, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 119
Implementing break on load #241
Merged
+410
−30
Merged
Changes from 2 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c0e2d38
Implementing break on load using Chrome's Instrumentation Breakpoint …
330568e
Refactoring code
61f3f23
Addressing feedback
731f7aa
Fixing failures
2bf4c1d
Making breakonload work when source maps are enabled
c296b4a
Small change for break on load tests in chrome-debug
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
/*--------------------------------------------------------- | ||
* Copyright (C) Microsoft Corporation. All rights reserved. | ||
*--------------------------------------------------------*/ | ||
|
||
import {logger} from 'vscode-debugadapter'; | ||
import {ISetBreakpointResult} from '../debugAdapterInterfaces'; | ||
|
||
import Crdp from '../../crdp/crdp'; | ||
import {ChromeDebugAdapter} from './chromeDebugAdapter'; | ||
|
||
import * as path from 'path'; | ||
|
||
export class BreakOnLoadHelper { | ||
|
||
public userBreakpointOnLine1Col1: boolean = false; | ||
private _instrumentationBreakpointSet: boolean = false; | ||
|
||
// 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 | ||
private _stopOnEntryBreakpointIdToRequestedFileName = new Map<string, [string, Set<string>]>(); | ||
private _stopOnEntryRequestedFileNameToBreakpointId = new Map<string, string>(); | ||
private _stopOnEntryRegexToBreakpointId = new Map<string, string>(); | ||
|
||
private _chromeDebugAdapter: ChromeDebugAdapter; | ||
|
||
public constructor(chromeDebugAdapter: ChromeDebugAdapter) { | ||
this._chromeDebugAdapter = chromeDebugAdapter; | ||
} | ||
|
||
public get stopOnEntryRequestedFileNameToBreakpointId(): Map<string, string> { | ||
return this._stopOnEntryRequestedFileNameToBreakpointId; | ||
} | ||
|
||
public get stopOnEntryBreakpointIdToRequestedFileName(): Map<string, [string, Set<string>]> { | ||
return this._stopOnEntryBreakpointIdToRequestedFileName; | ||
} | ||
|
||
public get instrumentationBreakpointSet(): boolean { | ||
return this._instrumentationBreakpointSet; | ||
} | ||
|
||
/** | ||
* Checks and resolves the pending breakpoints of a script. If any breakpoints were resolved returns true, else false. | ||
* Used when break on load active, either through Chrome's Instrumentation Breakpoint API or the regex approach | ||
*/ | ||
public async resolvePendingBreakpointsOfPausedScript(scriptId: string): Promise<boolean> { | ||
const pausedScriptUrl = this._chromeDebugAdapter.scriptsById.get(scriptId).url; | ||
const mappedUrl = await this._chromeDebugAdapter.pathTransformer.scriptParsed(pausedScriptUrl); | ||
|
||
const pendingBreakpoints = this._chromeDebugAdapter.pendingBreakpointsByUrl.get(mappedUrl); | ||
// If the file has unbound breakpoints, resolve them and return true | ||
if (pendingBreakpoints !== undefined) { | ||
await this._chromeDebugAdapter.resolvePendingBreakpoint(pendingBreakpoints); | ||
return true; | ||
} else { | ||
// If no pending breakpoints, return false | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Returns whether we should continue on hitting a stopOnEntry breakpoint | ||
* Only used when using regex approach for break on load | ||
*/ | ||
private async shouldContinueOnStopOnEntryBreakpoint(scriptId: string): Promise<boolean> { | ||
// 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 | ||
let shouldContinue = true; | ||
let anyPendingBreakpointsResolved = await this.resolvePendingBreakpointsOfPausedScript(scriptId); | ||
|
||
// If there were any pending breakpoints resolved and any of them was at (1,1) we shouldn't continue | ||
if (anyPendingBreakpointsResolved && this.userBreakpointOnLine1Col1) { | ||
// Here we need to store this information per file, but since we can safely assume that scriptParsed would immediately be followed by onPaused event | ||
// for the breakonload files, this implementation should be fine | ||
this.userBreakpointOnLine1Col1 = false; | ||
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 | ||
*/ | ||
public async handleStopOnEntryBreakpointAndContinue(notification: Crdp.Debugger.PausedEvent): Promise<boolean> { | ||
const hitBreakpoints = notification.hitBreakpoints; | ||
let allStopOnEntryBreakpoints = true; | ||
|
||
// If there is a breakpoint which is not a stopOnEntry breakpoint, we appear as if we hit that one | ||
// This is particularly done for cases when we end up with a user breakpoint and a stopOnEntry breakpoint on the same line | ||
hitBreakpoints.forEach(bp => { | ||
if (!this._stopOnEntryBreakpointIdToRequestedFileName.has(bp)) { | ||
notification.hitBreakpoints = [bp]; | ||
allStopOnEntryBreakpoints = false; | ||
} | ||
}); | ||
|
||
// If all the breakpoints on this point are stopOnEntry breakpoints | ||
// This will be true in cases where it's a single breakpoint and it's a stopOnEntry breakpoint | ||
// 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 | ||
// 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 | ||
// 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 | ||
// in itself. So when the file is actually loaded, we would have 2 stopOnEntry breakpoints */ | ||
|
||
if (allStopOnEntryBreakpoints) { | ||
const pausedScriptId = notification.callFrames[0].location.scriptId; | ||
let shouldContinue = await this.shouldContinueOnStopOnEntryBreakpoint(pausedScriptId); | ||
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 | ||
*/ | ||
public async addStopOnEntryBreakpoint(url: string): Promise<ISetBreakpointResult[]> { | ||
let responsePs: ISetBreakpointResult[]; | ||
// Check if file already has a stop on entry breakpoint | ||
if (!this._stopOnEntryRequestedFileNameToBreakpointId.has(url)) { | ||
|
||
// Generate regex we need for the file | ||
const urlRegex = this.getUrlRegexForBreakOnLoad(url); | ||
|
||
// 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 | ||
let breakpointId: string; | ||
breakpointId = this._stopOnEntryRegexToBreakpointId.get(urlRegex); | ||
|
||
// If breakpointId is undefined it means the breakpoint doesn't exist yet so we add it | ||
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 = []; | ||
} | ||
|
||
// Store the new breakpointId and the file name in the right mappings | ||
this._stopOnEntryRequestedFileNameToBreakpointId.set(url, breakpointId); | ||
|
||
let regexAndFileNames = this._stopOnEntryBreakpointIdToRequestedFileName.get(breakpointId); | ||
|
||
// If there already exists an entry for the breakpoint Id, we add this file to the list of file mappings | ||
if (regexAndFileNames !== undefined) { | ||
regexAndFileNames[1].add(url); | ||
} else { // else create an entry for this breakpoint id | ||
const fileSet = new Set<string>(); | ||
fileSet.add(url); | ||
this._stopOnEntryBreakpointIdToRequestedFileName.set(breakpointId, [urlRegex, fileSet]); | ||
} | ||
} else { | ||
responsePs = []; | ||
} | ||
return Promise.all(responsePs); | ||
} | ||
|
||
/** | ||
* Tells Chrome to set instrumentation breakpoint to stop on all the scripts before execution | ||
* Only used when using instrument approach for break on load | ||
*/ | ||
public async setInstrumentationBreakpoint(): Promise<void> { | ||
this._chromeDebugAdapter.chrome.DOMDebugger.setInstrumentationBreakpoint({eventName: "scriptFirstStatement"}); | ||
this._instrumentationBreakpointSet = true; | ||
} | ||
|
||
// Sets a breakpoint on (0,0) for the files matching the given regex | ||
private async setStopOnEntryBreakpoint(urlRegex: string): Promise<Crdp.Debugger.SetBreakpointByUrlResponse> { | ||
let result = await this._chromeDebugAdapter.chrome.Debugger.setBreakpointByUrl({ urlRegex, lineNumber: 0, columnNumber: 0, condition: '' }); | ||
return result; | ||
} | ||
|
||
/* Constructs the regex for files to enable break on load | ||
For example, for a file index.js the regex will match urls containing index.js, index.ts, abc/index.ts, index.bin.js etc | ||
It won't match index100.js, indexabc.ts etc */ | ||
private getUrlRegexForBreakOnLoad(url: string): string { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move to chromeUtils and write unit tests |
||
const fileNameWithoutFullPath = path.parse(url).base; | ||
const fileNameWithoutExtension = path.parse(fileNameWithoutFullPath).name; | ||
const escapedFileName = fileNameWithoutExtension.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||
|
||
return ".*[\\\\\\/]" + escapedFileName + "([^A-z^0-9].*)?$"; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just leave 'condition' out