vscode-chrome-debug-core
Version:
A library for building VS Code debug adapters for targets that support the Chrome Remote Debug Protocol
1,047 lines • 121 kB
JavaScript
"use strict";
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const vscode_debugadapter_1 = require("vscode-debugadapter");
const chromeConnection_1 = require("./chromeConnection");
const ChromeUtils = require("./chromeUtils");
const variables_1 = require("./variables");
const variables = require("./variables");
const consoleHelper_1 = require("./consoleHelper");
const stoppedEvent_1 = require("./stoppedEvent");
const internalSourceBreakpoint_1 = require("./internalSourceBreakpoint");
const errors = require("../errors");
const utils = require("../utils");
const utils_1 = require("../utils");
const telemetry_1 = require("../telemetry");
const executionTimingsReporter_1 = require("../executionTimingsReporter");
const lineNumberTransformer_1 = require("../transformers/lineNumberTransformer");
const remotePathTransformer_1 = require("../transformers/remotePathTransformer");
const eagerSourceMapTransformer_1 = require("../transformers/eagerSourceMapTransformer");
const fallbackToClientPathTransformer_1 = require("../transformers/fallbackToClientPathTransformer");
const breakOnLoadHelper_1 = require("./breakOnLoadHelper");
const sourceMapUtils = require("../sourceMaps/sourceMapUtils");
const path = require("path");
const nls = require("vscode-nls");
let localize = nls.loadMessageBundle(__filename);
class ChromeDebugAdapter {
constructor({ chromeConnection, lineColTransformer, sourceMapTransformer, pathTransformer, targetFilter, enableSourceMapCaching }, session) {
this._domains = new Map();
this._waitAfterStep = Promise.resolve();
this._blackboxedRegexes = [];
this._skipFileStatuses = new Map();
this._currentStep = Promise.resolve();
this._currentLogMessage = Promise.resolve();
this._nextUnboundBreakpointId = 0;
this._pauseOnPromiseRejections = true;
this._promiseRejectExceptionFilterEnabled = false;
this._smartStepCount = 0;
this._earlyScripts = [];
this._initialSourceMapsP = Promise.resolve();
// Queue to synchronize new source loaded and source removed events so that 'remove' script events
// won't be send before the corresponding 'new' event has been sent
this._sourceLoadedQueue = Promise.resolve(null);
// Promises so ScriptPaused events can wait for ScriptParsed events to finish resolving breakpoints
this._scriptIdToBreakpointsAreResolvedDefer = new Map();
this._loadedSourcesByScriptId = new Map();
telemetry_1.telemetry.setupEventHandler(e => session.sendEvent(e));
this._batchTelemetryReporter = new telemetry_1.BatchTelemetryReporter(telemetry_1.telemetry);
this._session = session;
this._chromeConnection = new (chromeConnection || chromeConnection_1.ChromeConnection)(undefined, targetFilter);
this.events = new executionTimingsReporter_1.StepProgressEventsEmitter(this._chromeConnection.events ? [this._chromeConnection.events] : []);
this._frameHandles = new vscode_debugadapter_1.Handles();
this._variableHandles = new variables.VariableHandles();
this._breakpointIdHandles = new utils.ReverseHandles();
this._sourceHandles = new utils.ReverseHandles();
this._pendingBreakpointsByUrl = new Map();
this._hitConditionBreakpointsById = new Map();
this._lineColTransformer = new (lineColTransformer || lineNumberTransformer_1.LineColTransformer)(this._session);
this._sourceMapTransformer = new (sourceMapTransformer || eagerSourceMapTransformer_1.EagerSourceMapTransformer)(this._sourceHandles, enableSourceMapCaching);
this._pathTransformer = new (pathTransformer || remotePathTransformer_1.RemotePathTransformer)();
this.clearTargetContext();
}
get chrome() {
return this._chromeConnection.api;
}
get scriptsById() {
return this._scriptsById;
}
get pathTransformer() {
return this._pathTransformer;
}
get pendingBreakpointsByUrl() {
return this._pendingBreakpointsByUrl;
}
get committedBreakpointsByUrl() {
return this._committedBreakpointsByUrl;
}
get sourceMapTransformer() {
return this._sourceMapTransformer;
}
/**
* Called on 'clearEverything' or on a navigation/refresh
*/
clearTargetContext() {
this._sourceMapTransformer.clearTargetContext();
this._scriptsById = new Map();
this._scriptsByUrl = new Map();
this._committedBreakpointsByUrl = new Map();
this._setBreakpointsRequestQ = Promise.resolve();
this._pathTransformer.clearTargetContext();
}
/* __GDPR__
"ClientRequest/initialize" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
initialize(args) {
if (args.supportsMapURLToFilePathRequest) {
this._pathTransformer = new fallbackToClientPathTransformer_1.FallbackToClientPathTransformer(this._session);
}
this._isVSClient = args.clientID === 'visualstudio';
utils.setCaseSensitivePaths(!this._isVSClient);
this._sourceMapTransformer.isVSClient = this._isVSClient;
if (args.pathFormat !== 'path') {
throw errors.pathFormat();
}
if (args.locale) {
localize = nls.config({ locale: args.locale })(__filename);
}
// because session bypasses dispatchRequest
if (typeof args.linesStartAt1 === 'boolean') {
this._clientLinesStartAt1 = args.linesStartAt1;
}
if (typeof args.columnsStartAt1 === 'boolean') {
this._clientColumnsStartAt1 = args.columnsStartAt1;
}
const exceptionBreakpointFilters = [
{
label: localize(0, null),
filter: 'all',
default: false
},
{
label: localize(1, null),
filter: 'uncaught',
default: false
}
];
if (this._promiseRejectExceptionFilterEnabled) {
exceptionBreakpointFilters.push({
label: localize(2, null),
filter: 'promise_reject',
default: false
});
}
// This debug adapter supports two exception breakpoint filters
return {
exceptionBreakpointFilters,
supportsConfigurationDoneRequest: true,
supportsSetVariable: true,
supportsConditionalBreakpoints: true,
supportsCompletionsRequest: true,
supportsHitConditionalBreakpoints: true,
supportsRestartFrame: true,
supportsExceptionInfoRequest: true,
supportsDelayedStackTraceLoading: true,
supportsValueFormattingOptions: true,
supportsEvaluateForHovers: true,
supportsLoadedSourcesRequest: true
};
}
/* __GDPR__
"ClientRequest/configurationDone" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
configurationDone() {
return Promise.resolve();
}
get breakOnLoadActive() {
return !!this._breakOnLoadHelper;
}
/* __GDPR__
"ClientRequest/launch" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
launch(args, telemetryPropertyCollector) {
return __awaiter(this, void 0, void 0, function* () {
this.commonArgs(args);
if (args.pathMapping) {
for (const urlToMap in args.pathMapping) {
args.pathMapping[urlToMap] = utils.canonicalizeUrl(args.pathMapping[urlToMap]);
}
}
this._sourceMapTransformer.launch(args);
this._pathTransformer.launch(args);
if (args.breakOnLoadStrategy && args.breakOnLoadStrategy !== 'off') {
this._breakOnLoadHelper = new breakOnLoadHelper_1.BreakOnLoadHelper(this, args.breakOnLoadStrategy);
}
if (!args.__restart) {
/* __GDPR__
"debugStarted" : {
"request" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"args" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [ "${DebugCommonProperties}" ]
}
*/
telemetry_1.telemetry.reportEvent('debugStarted', { request: 'launch', args: Object.keys(args) });
}
});
}
/* __GDPR__
"ClientRequest/attach" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
attach(args) {
return __awaiter(this, void 0, void 0, function* () {
this._attachMode = true;
this.commonArgs(args);
this._sourceMapTransformer.attach(args);
this._pathTransformer.attach(args);
if (!args.port) {
args.port = 9229;
}
/* __GDPR__
"debugStarted" : {
"request" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"args" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [ "${DebugCommonProperties}" ]
}
*/
telemetry_1.telemetry.reportEvent('debugStarted', { request: 'attach', args: Object.keys(args) });
yield this.doAttach(args.port, args.url, args.address, args.timeout, args.websocketUrl, args.extraCRDPChannelPort);
});
}
commonArgs(args) {
let logToFile = false;
let logLevel;
if (args.trace === 'verbose') {
logLevel = vscode_debugadapter_1.Logger.LogLevel.Verbose;
logToFile = true;
}
else if (args.trace) {
logLevel = vscode_debugadapter_1.Logger.LogLevel.Warn;
logToFile = true;
}
else {
logLevel = vscode_debugadapter_1.Logger.LogLevel.Warn;
}
// The debug configuration provider should have set logFilePath on the launch config. If not, default to 'true' to use the
// "legacy" log file path from the CDA subclass
const logFilePath = args.logFilePath || logToFile;
vscode_debugadapter_1.logger.setup(logLevel, logFilePath);
this._launchAttachArgs = args;
// Enable sourcemaps and async callstacks by default
args.sourceMaps = typeof args.sourceMaps === 'undefined' || args.sourceMaps;
this._smartStepEnabled = this._launchAttachArgs.smartStep;
}
shutdown() {
this._batchTelemetryReporter.finalize();
this._inShutdown = true;
this._session.shutdown();
}
terminateSession(reason, disconnectArgs, restart) {
return __awaiter(this, void 0, void 0, function* () {
vscode_debugadapter_1.logger.log(`Terminated: ${reason}`);
if (!this._hasTerminated) {
vscode_debugadapter_1.logger.log(`Waiting for any pending steps or log messages.`);
yield this._currentStep;
yield this._currentLogMessage;
vscode_debugadapter_1.logger.log(`Current step and log messages complete`);
/* __GDPR__
"debugStopped" : {
"reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"${include}": [ "${DebugCommonProperties}" ]
}
*/
telemetry_1.telemetry.reportEvent('debugStopped', { reason });
this._hasTerminated = true;
if (this._clientAttached || (this._launchAttachArgs && this._launchAttachArgs.noDebug)) {
this._session.sendEvent(new vscode_debugadapter_1.TerminatedEvent(restart));
}
if (this._chromeConnection.isAttached) {
this._chromeConnection.close();
}
}
});
}
/**
* Hook up all connection events
*/
hookConnectionEvents() {
this.chrome.Debugger.on('paused', params => {
/* __GDPR__
"target/notification/onPaused" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
this.runAndMeasureProcessingTime('target/notification/onPaused', () => __awaiter(this, void 0, void 0, function* () {
yield this.onPaused(params);
}));
});
this.chrome.Debugger.on('resumed', () => this.onResumed());
this.chrome.Debugger.on('scriptParsed', params => {
/* __GDPR__
"target/notification/onScriptParsed" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
this.runAndMeasureProcessingTime('target/notification/onScriptParsed', () => {
return this.onScriptParsed(params);
});
});
this.chrome.Debugger.on('breakpointResolved', params => this.onBreakpointResolved(params));
this.chrome.Console.on('messageAdded', params => this.onMessageAdded(params));
this.chrome.Runtime.on('consoleAPICalled', params => this.onConsoleAPICalled(params));
this.chrome.Runtime.on('exceptionThrown', params => this.onExceptionThrown(params));
this.chrome.Runtime.on('executionContextsCleared', () => this.onExecutionContextsCleared());
this.chrome.Log.on('entryAdded', params => this.onLogEntryAdded(params));
this._chromeConnection.onClose(() => this.terminateSession('websocket closed'));
}
runAndMeasureProcessingTime(notificationName, procedure) {
return __awaiter(this, void 0, void 0, function* () {
const startTime = Date.now();
const startTimeMark = process.hrtime();
let properties = {
startTime: startTime.toString()
};
try {
yield procedure();
properties.successful = 'true';
}
catch (e) {
properties.successful = 'false';
properties.exceptionType = 'firstChance';
utils.fillErrorDetails(properties, e);
}
const elapsedTime = utils.calculateElapsedTime(startTimeMark);
properties.timeTakenInMilliseconds = elapsedTime.toString();
// Callers set GDPR annotation
this._batchTelemetryReporter.reportEvent(notificationName, properties);
});
}
/**
* Enable clients and run connection
*/
runConnection() {
return [
this.chrome.Console.enable()
.catch(e => { }),
utils.toVoidP(this.chrome.Debugger.enable()),
this.chrome.Runtime.enable(),
this.chrome.Log.enable(),
this._chromeConnection.run(),
];
}
doAttach(port, targetUrl, address, timeout, websocketUrl, extraCRDPChannelPort) {
return __awaiter(this, void 0, void 0, function* () {
/* __GDPR__FRAGMENT__
"StepNames" : {
"Attach" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.events.emitStepStarted('Attach');
// Client is attaching - if not attached to the chrome target, create a connection and attach
this._clientAttached = true;
if (!this._chromeConnection.isAttached) {
if (websocketUrl) {
yield this._chromeConnection.attachToWebsocketUrl(websocketUrl, extraCRDPChannelPort);
}
else {
yield this._chromeConnection.attach(address, port, targetUrl, timeout, extraCRDPChannelPort);
}
/* __GDPR__FRAGMENT__
"StepNames" : {
"Attach.ConfigureDebuggingSession.Internal" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.events.emitStepStarted('Attach.ConfigureDebuggingSession.Internal');
this._port = port;
this.hookConnectionEvents();
let patterns = [];
if (this._launchAttachArgs.skipFiles) {
const skipFilesArgs = this._launchAttachArgs.skipFiles.filter(glob => {
if (glob.startsWith('!')) {
vscode_debugadapter_1.logger.warn(`Warning: skipFiles entries starting with '!' aren't supported and will be ignored. ("${glob}")`);
return false;
}
return true;
});
patterns = skipFilesArgs.map(glob => utils.pathGlobToBlackboxedRegex(glob));
}
if (this._launchAttachArgs.skipFileRegExps) {
patterns = patterns.concat(this._launchAttachArgs.skipFileRegExps);
}
/* __GDPR__FRAGMENT__
"StepNames" : {
"Attach.ConfigureDebuggingSession.Target" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this.events.emitStepStarted('Attach.ConfigureDebuggingSession.Target');
// Make sure debugging domain is enabled before calling refreshBlackboxPatterns() below
yield Promise.all(this.runConnection());
if (patterns.length) {
this._blackboxedRegexes = patterns.map(pattern => new RegExp(pattern, 'i'));
this.refreshBlackboxPatterns();
}
yield this.initSupportedDomains();
const maxDepth = this._launchAttachArgs.showAsyncStacks ? ChromeDebugAdapter.ASYNC_CALL_STACK_DEPTH : 0;
try {
yield this.chrome.Debugger.setAsyncCallStackDepth({ maxDepth });
}
catch (e) {
// Not supported by older runtimes, ignore it.
}
}
});
}
initSupportedDomains() {
return __awaiter(this, void 0, void 0, function* () {
try {
const domainResponse = yield this.chrome.Schema.getDomains();
domainResponse.domains.forEach(domain => this._domains.set(domain.name, domain));
}
catch (e) {
// If getDomains isn't supported for some reason, skip this
}
});
}
/**
* This event tells the client to begin sending setBP requests, etc. Some consumers need to override this
* to send it at a later time of their choosing.
*/
sendInitializedEvent() {
return __awaiter(this, void 0, void 0, function* () {
// Wait to finish loading sourcemaps from the initial scriptParsed events
if (this._initialSourceMapsP) {
const initialSourceMapsP = this._initialSourceMapsP;
this._initialSourceMapsP = null;
yield initialSourceMapsP;
this._session.sendEvent(new vscode_debugadapter_1.InitializedEvent());
this.events.emitStepCompleted('NotifyInitialized');
yield Promise.all(this._earlyScripts.map(script => this.sendLoadedSourceEvent(script)));
this._earlyScripts = null;
}
});
}
doAfterProcessingSourceEvents(action) {
return this._sourceLoadedQueue = this._sourceLoadedQueue.then(action);
}
/**
* e.g. the target navigated
*/
onExecutionContextsCleared() {
const cachedScriptParsedEvents = Array.from(this._scriptsById.values());
this.clearTargetContext();
return this.doAfterProcessingSourceEvents(() => __awaiter(this, void 0, void 0, function* () {
for (let scriptedParseEvent of cachedScriptParsedEvents) {
this.sendLoadedSourceEvent(scriptedParseEvent, 'removed');
}
}));
}
onPaused(notification, expectingStopReason = this._expectingStopReason) {
return __awaiter(this, void 0, void 0, function* () {
if (notification.asyncCallStackTraceId) {
yield this.chrome.Debugger.pauseOnAsyncCall({ parentStackTraceId: notification.asyncCallStackTraceId });
yield this.chrome.Debugger.resume();
return { didPause: false };
}
this._variableHandles.onPaused();
this._frameHandles.reset();
this._exception = undefined;
this._lastPauseState = { event: notification, expecting: expectingStopReason };
this._currentPauseNotification = notification;
// If break on load is active, we pass the notification object to breakonload helper
// If it returns true, we continue and return
if (this.breakOnLoadActive) {
let shouldContinue = yield this._breakOnLoadHelper.handleOnPaused(notification);
if (shouldContinue) {
this.chrome.Debugger.resume()
.catch(e => {
vscode_debugadapter_1.logger.error('Failed to resume due to exception: ' + e.message);
});
return { didPause: false };
}
}
// We can tell when we've broken on an exception. Otherwise if hitBreakpoints is set, assume we hit a
// breakpoint. If not set, assume it was a step. We can't tell the difference between step and 'break on anything'.
let reason;
let shouldSmartStep = false;
if (notification.reason === 'exception') {
reason = 'exception';
this._exception = notification.data;
}
else if (notification.reason === 'promiseRejection') {
reason = 'promise_rejection';
// After processing smartStep and so on, check whether we are paused on a promise rejection, and should continue past it
if (this._promiseRejectExceptionFilterEnabled && !this._pauseOnPromiseRejections) {
this.chrome.Debugger.resume()
.catch(e => { });
return { didPause: false };
}
this._exception = notification.data;
}
else if (notification.hitBreakpoints && notification.hitBreakpoints.length) {
reason = 'breakpoint';
// Did we hit a hit condition breakpoint?
for (let hitBp of notification.hitBreakpoints) {
if (this._hitConditionBreakpointsById.has(hitBp)) {
// Increment the hit count and check whether to pause
const hitConditionBp = this._hitConditionBreakpointsById.get(hitBp);
hitConditionBp.numHits++;
// Only resume if we didn't break for some user action (step, pause button)
if (!expectingStopReason && !hitConditionBp.shouldPause(hitConditionBp.numHits)) {
this.chrome.Debugger.resume()
.catch(e => { });
return { didPause: false };
}
}
}
}
else if (expectingStopReason) {
// If this was a step, check whether to smart step
reason = expectingStopReason;
shouldSmartStep = yield this.shouldSmartStep(this._currentPauseNotification.callFrames[0]);
}
else {
reason = 'debugger_statement';
}
this._expectingStopReason = undefined;
if (shouldSmartStep) {
this._smartStepCount++;
yield this.stepIn(false);
return { didPause: false };
}
else {
if (this._smartStepCount > 0) {
vscode_debugadapter_1.logger.log(`SmartStep: Skipped ${this._smartStepCount} steps`);
this._smartStepCount = 0;
}
// Enforce that the stopped event is not fired until we've sent the response to the step that induced it.
// Also with a timeout just to ensure things keep moving
const sendStoppedEvent = () => {
return this._session.sendEvent(new stoppedEvent_1.StoppedEvent2(reason, /*threadId=*/ ChromeDebugAdapter.THREAD_ID, this._exception));
};
yield utils.promiseTimeout(this._currentStep, /*timeoutMs=*/ 300)
.then(sendStoppedEvent, sendStoppedEvent);
return { didPause: true };
}
});
}
/* __GDPR__
"ClientRequest/exceptionInfo" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
exceptionInfo(args) {
return __awaiter(this, void 0, void 0, function* () {
if (args.threadId !== ChromeDebugAdapter.THREAD_ID) {
throw errors.invalidThread(args.threadId);
}
if (this._exception) {
const isError = this._exception.subtype === 'error';
const message = isError ? utils.firstLine(this._exception.description) : (this._exception.description || this._exception.value);
const formattedMessage = message && message.replace(/\*/g, '\\*');
const response = {
exceptionId: this._exception.className || this._exception.type || 'Error',
breakMode: 'unhandled',
details: {
stackTrace: this._exception.description && (yield this.mapFormattedException(this._exception.description)),
message,
formattedDescription: formattedMessage,
typeName: this._exception.subtype || this._exception.type
}
};
return response;
}
else {
throw errors.noStoredException();
}
});
}
shouldSmartStep(frame) {
return __awaiter(this, void 0, void 0, function* () {
if (!this._smartStepEnabled)
return Promise.resolve(false);
const stackFrame = this.callFrameToStackFrame(frame);
const clientPath = this._pathTransformer.getClientPathFromTargetPath(stackFrame.source.path) || stackFrame.source.path;
const mapping = yield this._sourceMapTransformer.mapToAuthored(clientPath, frame.location.lineNumber, frame.location.columnNumber);
if (mapping) {
return false;
}
if ((yield this.sourceMapTransformer.allSources(clientPath)).length) {
return true;
}
return false;
});
}
onResumed() {
this._currentPauseNotification = null;
if (this._expectingResumedEvent) {
this._expectingResumedEvent = false;
// Need to wait to eval just a little after each step, because of #148
this._waitAfterStep = utils.promiseTimeout(null, 50);
}
else {
let resumedEvent = new vscode_debugadapter_1.ContinuedEvent(ChromeDebugAdapter.THREAD_ID);
this._session.sendEvent(resumedEvent);
}
}
detectColumnBreakpointSupport(scriptId) {
return __awaiter(this, void 0, void 0, function* () {
this._columnBreakpointsEnabled = false; // So it isn't requested multiple times
try {
yield this.chrome.Debugger.getPossibleBreakpoints({
start: { scriptId, lineNumber: 0, columnNumber: 0 },
end: { scriptId, lineNumber: 1, columnNumber: 0 },
restrictToFunction: false
});
this._columnBreakpointsEnabled = true;
}
catch (e) {
this._columnBreakpointsEnabled = false;
}
this._lineColTransformer.columnBreakpointsEnabled = this._columnBreakpointsEnabled;
});
}
getBreakpointsResolvedDefer(scriptId) {
const existingValue = this._scriptIdToBreakpointsAreResolvedDefer.get(scriptId);
if (existingValue) {
return existingValue;
}
else {
const newValue = utils_1.promiseDefer();
this._scriptIdToBreakpointsAreResolvedDefer.set(scriptId, newValue);
return newValue;
}
}
onScriptParsed(script) {
return __awaiter(this, void 0, void 0, function* () {
// The stack trace and hash can be large and the DA doesn't need it.
delete script.stackTrace;
delete script.hash;
const breakpointsAreResolvedDefer = this.getBreakpointsResolvedDefer(script.scriptId);
try {
this.doAfterProcessingSourceEvents(() => __awaiter(this, void 0, void 0, function* () {
if (typeof this._columnBreakpointsEnabled === 'undefined') {
yield this.detectColumnBreakpointSupport(script.scriptId);
yield this.sendInitializedEvent();
}
if (this._earlyScripts) {
this._earlyScripts.push(script);
}
else {
yield this.sendLoadedSourceEvent(script);
}
}));
if (script.url) {
script.url = utils.fixDriveLetter(script.url);
}
else {
script.url = ChromeDebugAdapter.EVAL_NAME_PREFIX + script.scriptId;
}
this._scriptsById.set(script.scriptId, script);
this._scriptsByUrl.set(utils.canonicalizeUrl(script.url), script);
const mappedUrl = yield this._pathTransformer.scriptParsed(script.url);
const resolvePendingBPs = (source) => __awaiter(this, void 0, void 0, function* () {
source = source && utils.canonicalizeUrl(source);
const pendingBP = this._pendingBreakpointsByUrl.get(source);
if (pendingBP && (!pendingBP.setWithPath || utils.canonicalizeUrl(pendingBP.setWithPath) === source)) {
vscode_debugadapter_1.logger.log(`OnScriptParsed.resolvePendingBPs: Resolving pending breakpoints: ${JSON.stringify(pendingBP)}`);
yield this.resolvePendingBreakpoint(pendingBP);
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)) {
vscode_debugadapter_1.logger.log(`OnScriptParsed.resolvePendingBPs: The following pending breakpoints won't be resolved: ${JSON.stringify(pendingBP)} pendingBreakpointsByUrl = ${JSON.stringify([...this._pendingBreakpointsByUrl])} source = ${source}`);
}
}
});
const sourceMapsP = this._sourceMapTransformer.scriptParsed(mappedUrl, script.sourceMapURL).then((sources) => __awaiter(this, void 0, void 0, function* () {
if (this._hasTerminated) {
return undefined;
}
if (sources) {
const filteredSources = sources.filter(source => source !== mappedUrl); // Tools like babel-register will produce sources with the same path as the generated script
for (const filteredSource of filteredSources) {
yield resolvePendingBPs(filteredSource);
}
}
if (script.url === mappedUrl && this._pendingBreakpointsByUrl.has(mappedUrl) && this._pendingBreakpointsByUrl.get(mappedUrl).setWithPath === mappedUrl) {
// If the pathTransformer had no effect, and we attempted to set the BPs with that path earlier, then assume that they are about
// to be resolved in this loaded script, and remove the pendingBP.
this._pendingBreakpointsByUrl.delete(mappedUrl);
}
else {
yield resolvePendingBPs(mappedUrl);
}
yield this.resolveSkipFiles(script, mappedUrl, sources);
}));
if (this._initialSourceMapsP) {
this._initialSourceMapsP = Promise.all([this._initialSourceMapsP, sourceMapsP]);
}
yield sourceMapsP;
breakpointsAreResolvedDefer.resolve(); // By now no matter which code path we choose, resolving pending breakpoints should be finished, so trigger the defer
}
catch (exception) {
breakpointsAreResolvedDefer.reject(exception);
}
});
}
sendLoadedSourceEvent(script, loadedSourceEventReason = 'new') {
return __awaiter(this, void 0, void 0, function* () {
const source = yield this.scriptToSource(script);
// This is a workaround for an edge bug, see https://github.com/Microsoft/vscode-chrome-debug-core/pull/329
switch (loadedSourceEventReason) {
case 'new':
case 'changed':
if (this._loadedSourcesByScriptId.get(script.scriptId)) {
loadedSourceEventReason = 'changed';
}
else {
loadedSourceEventReason = 'new';
}
this._loadedSourcesByScriptId.set(script.scriptId, script);
break;
case 'removed':
if (!this._loadedSourcesByScriptId.delete(script.scriptId)) {
telemetry_1.telemetry.reportEvent('LoadedSourceEventError', { issue: 'Tried to remove non-existent script', scriptId: script.scriptId });
return;
}
break;
default:
telemetry_1.telemetry.reportEvent('LoadedSourceEventError', { issue: 'Unknown reason', reason: loadedSourceEventReason });
}
const scriptEvent = new vscode_debugadapter_1.LoadedSourceEvent(loadedSourceEventReason, source);
this._session.sendEvent(scriptEvent);
});
}
resolveSkipFiles(script, mappedUrl, sources, toggling) {
return __awaiter(this, void 0, void 0, function* () {
if (sources && sources.length) {
const parentIsSkipped = this.shouldSkipSource(script.url);
const libPositions = [];
// Figure out skip/noskip transitions within script
let inLibRange = parentIsSkipped;
for (let s of sources) {
let isSkippedFile = this.shouldSkipSource(s);
if (typeof isSkippedFile !== 'boolean') {
// Inherit the parent's status
isSkippedFile = parentIsSkipped;
}
this._skipFileStatuses.set(s, isSkippedFile);
if ((isSkippedFile && !inLibRange) || (!isSkippedFile && inLibRange)) {
const details = yield this.sourceMapTransformer.allSourcePathDetails(mappedUrl);
const detail = details.find(d => d.inferredPath === s);
libPositions.push({
lineNumber: detail.startPosition.line,
columnNumber: detail.startPosition.column
});
inLibRange = !inLibRange;
}
}
// If there's any change from the default, set proper blackboxed ranges
if (libPositions.length || toggling) {
if (parentIsSkipped) {
libPositions.splice(0, 0, { lineNumber: 0, columnNumber: 0 });
}
if (libPositions[0].lineNumber !== 0 || libPositions[0].columnNumber !== 0) {
// The list of blackboxed ranges must start with 0,0 for some reason.
// https://github.com/Microsoft/vscode-chrome-debug/issues/667
libPositions[0] = {
lineNumber: 0,
columnNumber: 0
};
}
yield this.chrome.Debugger.setBlackboxedRanges({
scriptId: script.scriptId,
positions: []
}).catch(() => this.warnNoSkipFiles());
if (libPositions.length) {
this.chrome.Debugger.setBlackboxedRanges({
scriptId: script.scriptId,
positions: libPositions
}).catch(() => this.warnNoSkipFiles());
}
}
}
else {
const status = yield this.getSkipStatus(mappedUrl);
const skippedByPattern = this.matchesSkipFilesPatterns(mappedUrl);
if (typeof status === 'boolean' && status !== skippedByPattern) {
const positions = status ? [{ lineNumber: 0, columnNumber: 0 }] : [];
this.chrome.Debugger.setBlackboxedRanges({
scriptId: script.scriptId,
positions
}).catch(() => this.warnNoSkipFiles());
}
}
});
}
warnNoSkipFiles() {
vscode_debugadapter_1.logger.log('Warning: this runtime does not support skipFiles');
}
/**
* If the source has a saved skip status, return that, whether true or false.
* If not, check it against the patterns list.
*/
shouldSkipSource(sourcePath) {
const status = this.getSkipStatus(sourcePath);
if (typeof status === 'boolean') {
return status;
}
if (this.matchesSkipFilesPatterns(sourcePath)) {
return true;
}
return undefined;
}
/**
* Returns true if this path matches one of the static skip patterns
*/
matchesSkipFilesPatterns(sourcePath) {
return this._blackboxedRegexes.some(regex => {
return regex.test(sourcePath);
});
}
/**
* Returns the current skip status for this path, which is either an authored or generated script.
*/
getSkipStatus(sourcePath) {
if (this._skipFileStatuses.has(sourcePath)) {
return this._skipFileStatuses.get(sourcePath);
}
return undefined;
}
/* __GDPR__
"ClientRequest/toggleSmartStep" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
toggleSmartStep() {
return __awaiter(this, void 0, void 0, function* () {
this._smartStepEnabled = !this._smartStepEnabled;
this.onPaused(this._lastPauseState.event, this._lastPauseState.expecting);
});
}
/* __GDPR__
"ClientRequest/toggleSkipFileStatus" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
toggleSkipFileStatus(args) {
return __awaiter(this, void 0, void 0, function* () {
if (args.path) {
args.path = utils.fileUrlToPath(args.path);
}
if (!(yield this.isInCurrentStack(args))) {
// Only valid for files that are in the current stack
const logName = args.path || this.displayNameForSourceReference(args.sourceReference);
vscode_debugadapter_1.logger.log(`Can't toggle the skipFile status for ${logName} - it's not in the current stack.`);
return;
}
// e.g. strip <node_internals>/
if (args.path) {
args.path = this.displayPathToRealPath(args.path);
}
const aPath = args.path || this.fakeUrlForSourceReference(args.sourceReference);
const generatedPath = yield this._sourceMapTransformer.getGeneratedPathFromAuthoredPath(aPath);
if (!generatedPath) {
vscode_debugadapter_1.logger.log(`Can't toggle the skipFile status for: ${aPath} - haven't seen it yet.`);
return;
}
const sources = yield this._sourceMapTransformer.allSources(generatedPath);
if (generatedPath === aPath && sources.length) {
// Ignore toggling skip status for generated scripts with sources
vscode_debugadapter_1.logger.log(`Can't toggle skipFile status for ${aPath} - it's a script with a sourcemap`);
return;
}
const newStatus = !this.shouldSkipSource(aPath);
vscode_debugadapter_1.logger.log(`Setting the skip file status for: ${aPath} to ${newStatus}`);
this._skipFileStatuses.set(aPath, newStatus);
const targetPath = this._pathTransformer.getTargetPathFromClientPath(generatedPath) || generatedPath;
const script = this.getScriptByUrl(targetPath);
yield this.resolveSkipFiles(script, generatedPath, sources, /*toggling=*/ true);
if (newStatus) {
this.makeRegexesSkip(script.url);
}
else {
this.makeRegexesNotSkip(script.url);
}
this.onPaused(this._lastPauseState.event, this._lastPauseState.expecting);
});
}
isInCurrentStack(args) {
return __awaiter(this, void 0, void 0, function* () {
const currentStack = yield this.stackTrace({ threadId: undefined });
if (args.path) {
return currentStack.stackFrames.some(frame => frame.source && frame.source.path === args.path);
}
else {
return currentStack.stackFrames.some(frame => frame.source && frame.source.sourceReference === args.sourceReference);
}
});
}
makeRegexesNotSkip(noSkipPath) {
let somethingChanged = false;
this._blackboxedRegexes = this._blackboxedRegexes.map(regex => {
const result = utils.makeRegexNotMatchPath(regex, noSkipPath);
somethingChanged = somethingChanged || (result !== regex);
return result;
});
if (somethingChanged) {
this.refreshBlackboxPatterns();
}
}
makeRegexesSkip(skipPath) {
let somethingChanged = false;
this._blackboxedRegexes = this._blackboxedRegexes.map(regex => {
const result = utils.makeRegexMatchPath(regex, skipPath);
somethingChanged = somethingChanged || (result !== regex);
return result;
});
if (!somethingChanged) {
this._blackboxedRegexes.push(new RegExp(utils.pathToRegex(skipPath), 'i'));
}
this.refreshBlackboxPatterns();
}
refreshBlackboxPatterns() {
this.chrome.Debugger.setBlackboxPatterns({
patterns: this._blackboxedRegexes.map(regex => regex.source)
}).catch(() => this.warnNoSkipFiles());
}
/* __GDPR__
"ClientRequest/loadedSources" : {
"${include}": [
"${IExecutionResultTelemetryProperties}",
"${DebugCommonProperties}"
]
}
*/
loadedSources(args) {
return __awaiter(this, void 0, void 0, function* () {
const sources = yield Promise.all(Array.from(this._scriptsByUrl.values())
.map(script => this.scriptToSource(script)));
return { sources: sources.sort((a, b) => a.path.localeCompare(b.path)) };
});
}
resolvePendingBreakpoint(pendingBP) {
return this.setBreakpoints(pendingBP.args, null, pendingBP.requestSeq, pendingBP.ids).then(response => {
response.breakpoints.forEach((bp, i) => {
bp.id = pendingBP.ids[i];
this._session.sendEvent(new vscode_debugadapter_1.BreakpointEvent('changed', bp));
});
});
}
onBreakpointResolved(params) {
const script = this._scriptsById.get(params.location.scriptId);
const breakpointId = this._breakpointIdHandles.lookup(params.breakpointId);
if (!script || !breakpointId) {
// Breakpoint resolved for a script we don't know about or a breakpoint we don't know about
return;
}
// If the breakpoint resolved is a stopOnEntry breakpoint, we just return since we don't need to send it to client
if (this.breakOnLoadActive && this._breakOnLoadHelper.stopOnEntryBreakpointIdToRequestedFileName.has(params.breakpointId)) {
return;
}
const committedBps = this._committedBreakpointsByUrl.get(script.url) || [];
if (!committedBps.find(committedBp => committedBp.breakpointId === params.breakpointId)) {
committedBps.push({ breakpointId: params.breakpointId, actualLocation: params.location });
}
this._committedBreakpointsByUrl.set(script.url, committedBps);
const bp = {
id: breakpointId,
verified: true,
line: params.location.lineNumber,
column: params.location.columnNumber
};
const scriptPath = this._pathTransformer.breakpointResolved(bp, script.url);
if (this._pendingBreakpointsByUrl.has(scriptPath)) {
// If we set these BPs before the script was loaded, remove from the pending list
this._pendingBreakpointsByUrl.delete(scriptPath);
}
this._sourceMapTransformer.breakpointResolved(bp, scriptPath);
this._lineColTransformer.breakpointResolved(bp);
this._session.sendEvent(new vscode_debugadapter_1.BreakpointEvent('changed', bp));
}
onConsoleAPICalled(event) {
if (this._launchAttachArgs._suppressConsoleOutput) {
return;
}
const result = consoleHelper_1.formatConsoleArguments(event.type, event.args, event.stackTrace);
const stack = internalSourceBreakpoint_1.stackTraceWithoutLogpointFrame(event.stackTrace);
if (result) {
this.logObjects(result.args, result.isError, stack);
}
}
onLogEntryAdded(event) {
// The Debug Console doesn't give the user a way to filter by level, just ignore 'verbose' logs
if (event.entry.level === 'verbose') {
return;
}
const args = event.entry.args || [];
let text = event.entry.text || '';
if (event.entry.url && !event.entry.stackTrace) {
if (text) {
text += ' ';
}
text += `[${event.entry.url}]`;
}
if (text) {
args.unshift({
type: 'string',
value: text
});
}
const type = event.entry.level === 'error' ? 'error' :
event.entry.level === 'warning' ? 'warning' :
'log';
const result = consoleHelper_1.formatConsoleArguments(type, args, event.entry.stackTrace);
const stack = event.entry.stackTrace;
if (result) {
this.logObjects(result.args, result.isError, stack);
}
}
logObjects(objs, isError = false, stackTrace) {
return __awaiter(this, void 0, void 0, function* () {
// This is an asynchronous method, so ensure that we handle one at a time so that they are sent out in the same order that they came in.
this._currentLogMessage = this._