From 318bc530aa9be3764844ba54bcd5ff87af7bca1d Mon Sep 17 00:00:00 2001 From: Michael Hayes Date: Wed, 9 Dec 2015 10:17:18 -0800 Subject: [PATCH] support sourcemaps in vm.runIn*Context --- src/node/nodeDebug.ts | 99 ++++++++++++++++++++++++++---- src/node/sourceMaps.ts | 134 ++++++++++++++++++++++------------------- 2 files changed, 159 insertions(+), 74 deletions(-) diff --git a/src/node/nodeDebug.ts b/src/node/nodeDebug.ts index 0eaddf1c..e7650f09 100644 --- a/src/node/nodeDebug.ts +++ b/src/node/nodeDebug.ts @@ -165,6 +165,7 @@ export class NodeDebugSession extends DebugSession { private static EXCEPTION_REASON = "exception"; private static DEBUGGER_REASON = "debugger statement"; private static USER_REQUEST_REASON = "user request"; + private static FIRST_LINE_REASON = "first line break"; private static ANON_FUNCTION = "(anonymous function)"; @@ -176,6 +177,7 @@ export class NodeDebugSession extends DebugSession { public _variableHandles = new Handles(); public _frameHandles = new Handles(); private _refCache = new Map(); + private _breakpointMap = new Map>(); private _externalConsole: boolean; private _isTerminated: boolean; @@ -193,6 +195,7 @@ export class NodeDebugSession extends DebugSession { private _needContinue: boolean; private _needBreakpointEvent: boolean; private _lazy: boolean; // whether node is in 'lazy' mode + private _firstLineBreakId: number; private _gotEntryEvent: boolean; private _entryPath: string; @@ -211,6 +214,8 @@ export class NodeDebugSession extends DebugSession { this._lastStoppedEvent = this.createStoppedEvent(event.body); if (this._lastStoppedEvent.body.reason === NodeDebugSession.ENTRY_REASON) { if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: supressed stop-on-entry event'); + } else if (this._lastStoppedEvent.body.reason === NodeDebugSession.FIRST_LINE_REASON) { + this._handleFirstLineBreak(event); } else { this.sendEvent(this._lastStoppedEvent); } @@ -245,6 +250,48 @@ export class NodeDebugSession extends DebugSession { this._refCache = new Map(); } + private _handleFirstLineBreak(breakEvent: NodeV8Event) { + this._node.command('scripts', { + ids: [breakEvent.body.script.id], + includeSource: true, + }, (sourceResponse: NodeV8Response) => { + let source = sourceResponse.body[0]; + this._sourceMaps.ParseGeneratedSource(source.name, source.source); + let mr = this._sourceMaps.MapToSource(source.name, 0, 0); + + if (!mr) { + this._node.command('continue'); + return; + } + + let originalPath = mr.path; + let lbs = this._breakpointMap.get(originalPath); + this._breakpointMap.delete(originalPath); + + if (!lbs || !lbs.length) { + this._node.command('continue'); + return; + } + + for (let i = 0; i < lbs.length; i++) { + const mr = this._sourceMaps.MapFromSource(originalPath, lbs[i].line, lbs[i].column); + if (mr) { + lbs[i].line = mr.line; + lbs[i].column = mr.column; + lbs[i].ignore = false; + } else { + lbs[i].ignore = true; + } + } + + this._clearAllBreakpointsFromPathOrScript(source.name, null, response => { + this._setBreakpoints(0, source.name, null, lbs, true, () => { + this._node.command('continue'); + }); + }); + }); + } + /** * The debug session has terminated. */ @@ -650,10 +697,10 @@ export class NodeDebugSession extends DebugSession { if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: 2nd retrieve node pid: OK'); this._nodeProcessId = parseInt(resp.body.value); } - this._middleInitialize(stopped); + this._addFirstLineBreaks(stopped); }); } else { - this._middleInitialize(stopped); + this._addFirstLineBreaks(stopped); } } else { if (NodeDebugSession.TRACE_INITIALISATION) console.error(`_init: no entry event after ${n} retries; give up`); @@ -667,11 +714,22 @@ export class NodeDebugSession extends DebugSession { this.rememberEntryLocation(s.name, resp.body.line, resp.body.column); } - this._middleInitialize(stopped); + this._addFirstLineBreaks(stopped); }); } } + private _addFirstLineBreaks(stopped: boolean): void { + let debugSession = this; + this._node.command('setbreakpoint', { + type: 'scriptRegExp', + target: '.*(?!\\.js$)\\.[^.]+$' + }, (data: any) => { + debugSession._firstLineBreakId = data.body.breakpoint; + debugSession._middleInitialize(stopped); + }); + } + private _middleInitialize(stopped: boolean): void { // request UI to send breakpoints if (NodeDebugSession.TRACE_INITIALISATION) console.error('_init: -> fire initialize event'); @@ -816,12 +874,20 @@ export class NodeDebugSession extends DebugSession { } path = p; } - else if (!NodeDebugSession.isJavaScript(path)) { - // ignore all breakpoints for this source - for (let lb of lbs) { - lb.ignore = true; + else { + // we might get a sourcemap for this path later + if (path) { + this._breakpointMap.set(path, lbs); + } + + if (!NodeDebugSession.isJavaScript(path)) { + // ignore all breakpoints for this source + for (let lb of lbs) { + lb.ignore = true; + } } } + this._clearAllBreakpoints(response, path, -1, lbs, sourcemap); return; } @@ -855,8 +921,17 @@ export class NodeDebugSession extends DebugSession { private _clearAllBreakpoints(response: DebugProtocol.SetBreakpointsResponse, path: string, scriptId: number, lbs: InternalBreakpoint[], sourcemap: boolean): void { // clear all existing breakpoints for the given path or script ID - this._node.command('listbreakpoints', null, (nodeResponse: NodeV8Response) => { + this._clearAllBreakpointsFromPathOrScript(path, scriptId, nodeResponse => { + if (nodeResponse.success) { + this._finishSetBreakpoints(response, path, scriptId, lbs, sourcemap); + } else { + this.sendNodeResponse(response, nodeResponse); + } + }) + } + private _clearAllBreakpointsFromPathOrScript(path: string, scriptId: number, done: (NodeV8Response) => void) : void { + this._node.command('listbreakpoints', null, (nodeResponse: NodeV8Response) => { if (nodeResponse.success) { const toClear = new Array(); @@ -880,15 +955,15 @@ export class NodeDebugSession extends DebugSession { } this._clearBreakpoints(toClear, 0, () => { - this._finishSetBreakpoints(response, path, scriptId, lbs, sourcemap); + done(nodeResponse); }); } else { - this.sendNodeResponse(response, nodeResponse); + done(nodeResponse); } }); - } + } /** * Recursive function for deleting node breakpoints. @@ -1920,6 +1995,8 @@ export class NodeDebugSession extends DebugSession { const line = body.sourceLine; const column = body.sourceColumn; this.rememberEntryLocation(path, line, column); + } else if (id === this._firstLineBreakId) { + reason = NodeDebugSession.FIRST_LINE_REASON; } else { reason = NodeDebugSession.BREAKPOINT_REASON; } diff --git a/src/node/sourceMaps.ts b/src/node/sourceMaps.ts index 64853409..7b827413 100644 --- a/src/node/sourceMaps.ts +++ b/src/node/sourceMaps.ts @@ -33,6 +33,11 @@ export interface ISourceMaps { * line and column are 0 based. */ MapToSource(path: string, line: number, column: number): MappingResult; + + /* + * Parse generated source and save mappings for that source if found. + */ + ParseGeneratedSource(pathToGenerated: string, generatedSource: string): void; } @@ -41,6 +46,7 @@ export class SourceMaps implements ISourceMaps { public static TRACE = false; private static SOURCE_MAPPING_MATCHER = new RegExp("//[#@] ?sourceMappingURL=(.+)$"); + private static DATA_URI_MATCHER = new RegExp('data:application\/json(?:;(.*?|\b)),(.*)$'); private _generatedToSourceMaps: { [id: string] : SourceMap; } = {}; // generated -> source file private _sourceToGeneratedMaps: { [id: string] : SourceMap; } = {}; // source file -> generated @@ -84,6 +90,51 @@ export class SourceMaps implements ISourceMaps { return null; } + public ParseGeneratedSource(pathToGenerated: string, generatedSource: string): SourceMap { + let map: SourceMap = null; + + // try to find a source map URL in the generated source + let map_path: string = null; + const uri = this._findSourceMapInGeneratedSource(generatedSource); + if (uri) { + const uriParts = uri.match(SourceMaps.DATA_URI_MATCHER); + if (uriParts) { + const json = new Buffer(uriParts[2], uriParts[1] || 'utf8').toString(); + try { + if (json) { + map = new SourceMap(pathToGenerated, json); + this._generatedToSourceMaps[pathToGenerated] = map; + return; + } + } + catch (e) { + console.error(`FindGeneratedToSourceMapping: exception while processing data url (${e})`); + return; + } + } + else { + map_path = uri; + } + } + + // if path is relative make it absolute + if (map_path && !Path.isAbsolute(map_path)) { + map_path = PathUtils.makePathAbsolute(pathToGenerated, map_path); + } + + if (map_path === null || !FS.existsSync(map_path)) { + // try to find map file next to the generated source + map_path = pathToGenerated + ".map"; + } + + if (FS.existsSync(map_path)) { + map = this._createSourceMap(map_path, pathToGenerated); + if (map) { + this._generatedToSourceMaps[pathToGenerated] = map; + } + } + } + //---- private ----------------------------------------------------------------------- private _findSourceToGeneratedMapping(pathToSource: string): SourceMap { @@ -150,60 +201,22 @@ export class SourceMaps implements ISourceMaps { } private _findGeneratedToSourceMapping(pathToGenerated: string): SourceMap { + if (pathToGenerated in this._generatedToSourceMaps) { + return this._generatedToSourceMaps[pathToGenerated]; + } if (pathToGenerated) { - - if (pathToGenerated in this._generatedToSourceMaps) { - return this._generatedToSourceMaps[pathToGenerated]; - } - - let map: SourceMap = null; - - // try to find a source map URL in the generated source - let map_path: string = null; - const uri = this._findSourceMapInGeneratedSource(pathToGenerated); - if (uri) { - if (uri.indexOf("data:application/json;base64,") >= 0) { - const pos = uri.indexOf(','); - if (pos > 0) { - const data = uri.substr(pos+1); - try { - const buffer = new Buffer(data, 'base64'); - const json = buffer.toString(); - if (json) { - map = new SourceMap(pathToGenerated, json); - this._generatedToSourceMaps[pathToGenerated] = map; - return map; - } - } - catch (e) { - console.error(`FindGeneratedToSourceMapping: exception while processing data url (${e})`); - } - } - } else { - map_path = uri; - } - } - - // if path is relative make it absolute - if (map_path && !Path.isAbsolute(map_path)) { - map_path = PathUtils.makePathAbsolute(pathToGenerated, map_path); - } - - if (map_path === null || !FS.existsSync(map_path)) { - // try to find map file next to the generated source - map_path = pathToGenerated + ".map"; + let contents; + try { + contents = FS.readFileSync(pathToGenerated).toString(); + } catch (e) { + return null; } - if (FS.existsSync(map_path)) { - map = this._createSourceMap(map_path, pathToGenerated); - if (map) { - this._generatedToSourceMaps[pathToGenerated] = map; - return map; - } - } + this.ParseGeneratedSource(pathToGenerated, contents); } - return null; + + return this._generatedToSourceMaps[pathToGenerated] || null; } private _createSourceMap(map_path: string, path: string): SourceMap { @@ -218,21 +231,16 @@ export class SourceMaps implements ISourceMaps { } // find "//# sourceMappingURL=" - private _findSourceMapInGeneratedSource(pathToGenerated: string): string { - - try { - const contents = FS.readFileSync(pathToGenerated).toString(); - const lines = contents.split('\n'); - for (let line of lines) { - const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); - if (matches && matches.length === 2) { - const uri = matches[1].trim(); - return uri; - } + private _findSourceMapInGeneratedSource(contents: string): string { + const lines = contents.split('\n'); + for (let line of lines) { + const matches = SourceMaps.SOURCE_MAPPING_MATCHER.exec(line); + if (matches && matches.length === 2) { + const uri = matches[1].trim(); + return uri; } - } catch (e) { - // ignore exception } + return null; } } @@ -283,7 +291,7 @@ class SourceMap { */ public doesOriginateFrom(absPath: string): boolean { for (let name of this._sources) { - const p = Path.join(this._sourceRoot, name); + const p = Path.resolve(this._sourceRoot, name); if (p === absPath) { return true; }