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

Implementing break on load #241

Merged
merged 6 commits into from
Nov 28, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions src/chrome/breakOnLoadHelper.ts
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: '' });
Copy link
Member

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

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 {
Copy link
Member

Choose a reason for hiding this comment

The 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].*)?$";
}
}
Loading