UNPKG

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
"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._