Skip to content
This repository has been archived by the owner on Oct 2, 2021. It is now read-only.

Commit

Permalink
Merge pull request #241 from rakatyal/breakonload
Browse files Browse the repository at this point in the history
Implementing break on load
  • Loading branch information
roblourens authored Nov 28, 2017
2 parents f5c21fc + c296b4a commit 22fbf5f
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 30 deletions.
263 changes: 263 additions & 0 deletions src/chrome/breakOnLoadHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import {logger} from 'vscode-debugadapter';
import {ISetBreakpointResult, BreakOnLoadStrategy} from '../debugAdapterInterfaces';

import Crdp from '../../crdp/crdp';
import {ChromeDebugAdapter} from './chromeDebugAdapter';
import * as ChromeUtils from './chromeUtils';

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;
private _breakOnLoadStrategy: BreakOnLoadStrategy;

public constructor(chromeDebugAdapter: ChromeDebugAdapter, breakOnLoadStrategy: BreakOnLoadStrategy) {
this._chromeDebugAdapter = chromeDebugAdapter;
this._breakOnLoadStrategy = breakOnLoadStrategy;
}

public get stopOnEntryRequestedFileNameToBreakpointId(): Map<string, string> {
return this._stopOnEntryRequestedFileNameToBreakpointId;
}

public get stopOnEntryBreakpointIdToRequestedFileName(): Map<string, [string, Set<string>]> {
return this._stopOnEntryBreakpointIdToRequestedFileName;
}

private get instrumentationBreakpointSet(): boolean {
return this._instrumentationBreakpointSet;
}

/**
* Checks and resolves the pending breakpoints of a script given it's source. 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
*/
private async resolvePendingBreakpoints(source: string): Promise<boolean> {
const pendingBreakpoints = this._chromeDebugAdapter.pendingBreakpointsByUrl.get(source);
// If the file has unbound breakpoints, resolve them and return true
if (pendingBreakpoints !== undefined) {
await this._chromeDebugAdapter.resolvePendingBreakpoint(pendingBreakpoints);
this._chromeDebugAdapter.pendingBreakpointsByUrl.delete(source);
return true;
} else {
// If no pending breakpoints, return false
return false;
}
}

/**
* Checks and resolves the pending breakpoints given a script Id. 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
*/
private async resolvePendingBreakpointsOfPausedScript(scriptId: string): Promise<boolean> {
const pausedScriptUrl = this._chromeDebugAdapter.scriptsById.get(scriptId).url;
const sourceMapUrl = this._chromeDebugAdapter.scriptsById.get(scriptId).sourceMapURL;
const mappedUrl = await this._chromeDebugAdapter.pathTransformer.scriptParsed(pausedScriptUrl);
let breakpointsResolved = false;

let sources = await this._chromeDebugAdapter.sourceMapTransformer.scriptParsed(mappedUrl, sourceMapUrl);

// If user breakpoint was put in a typescript file, pendingBreakpoints would store the typescript file in the mapping, so we need to hit those
if (sources) {
for (let source of sources) {
let anySourceBPResolved = await this.resolvePendingBreakpoints(source);
// If any of the source files had breakpoints resolved, we should return true
breakpointsResolved = breakpointsResolved || anySourceBPResolved;
}
}
// If sources is not present or user breakpoint was put in a compiled javascript file
let scriptBPResolved = await this.resolvePendingBreakpoints(mappedUrl);
breakpointsResolved = breakpointsResolved || scriptBPResolved;

return breakpointsResolved;
}

/**
* 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: Crdp.Debugger.PausedEvent): Promise<boolean> {
if (notification.hitBreakpoints && notification.hitBreakpoints.length) {
// If breakOnLoadStrategy is set to regex, we may have hit a stopOnEntry breakpoint we put.
// So we need to resolve all the pending breakpoints in this script and then decide to continue or not
if (this._breakOnLoadStrategy === 'regex') {
let shouldContinue = await this.handleStopOnEntryBreakpointAndContinue(notification);
return shouldContinue;
}
} else if (notification.reason === 'EventListener' && notification.data.eventName === "instrumentation:scriptFirstStatement" ) {
// This is fired when Chrome stops on the first line of a script when using the setInstrumentationBreakpoint API

const pausedScriptId = notification.callFrames[0].location.scriptId;
// Now we should resolve all the pending breakpoints and then continue
await this.resolvePendingBreakpointsOfPausedScript(pausedScriptId);
return true;
}
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
*/
private 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
*/
private 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 = ChromeUtils.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);
}

/**
* Handles the AddBreakpoints request when break on load is active
* Takes the action based on the strategy
*/
public async handleAddBreakpoints(url: string): Promise<ISetBreakpointResult[]> {
// If the strategy is set to regex, we try to match the file where user put the breakpoint through a regex and tell Chrome to put a stop on entry breakpoint there
if (this._breakOnLoadStrategy === 'regex') {
return this.addStopOnEntryBreakpoint(url);
} else if (this._breakOnLoadStrategy === 'instrument') {
// Else if strategy is to use Chrome's experimental instrumentation API, we stop on all the scripts at the first statement before execution
if (!this.instrumentationBreakpointSet) {
await this.setInstrumentationBreakpoint();
}
return [];
}
return undefined;
}

/**
* 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(): 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 });
return result;
}

/**
* 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: string): boolean {
if (this._breakOnLoadStrategy === 'regex' && !this.stopOnEntryRequestedFileNameToBreakpointId.has(mappedUrl)) {
return true;
}
return false;
}
}
Loading

0 comments on commit 22fbf5f

Please sign in to comment.