UNPKG

@inst/vscode-bin-darwin

Version:

BINARY ONLY - VSCode binary deployment for macOS

1,165 lines 145 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const vscode_debugadapter_1 = require("vscode-debugadapter"); const nodeV8Protocol_1 = require("./nodeV8Protocol"); const sourceMaps_1 = require("./sourceMaps"); const PathUtils = require("./pathUtilities"); const WSL = require("./wslSupport"); const CP = require("child_process"); const Net = require("net"); const URL = require("url"); const Path = require("path"); const FS = require("fs"); const nls = require("vscode-nls"); let localize = nls.config(process.env.VSCODE_NLS_CONFIG)(__filename); class Expander { constructor(func) { this._expanderFunction = func; } Expand(session, filter, start, count) { return this._expanderFunction(start, count); } SetValue(session, name, value) { return Promise.reject(new Error(Expander.SET_VALUE_ERROR)); } } Expander.SET_VALUE_ERROR = localize(0, null); exports.Expander = Expander; class PropertyContainer { constructor(evalName, obj, ths) { this._evalName = evalName; this._object = obj; this._this = ths; } Expand(session, filter, start, count) { if (filter === 'named') { return session._createProperties(this._evalName, this._object, 'named').then(variables => { if (this._this) { return session._createVariable(this._evalName, 'this', this._this).then(variable => { if (variable) { variables.push(variable); } return variables; }); } else { return variables; } }); } if (typeof start === 'number' && typeof count === 'number') { return session._createProperties(this._evalName, this._object, 'indexed', start, count); } else { return session._createProperties(this._evalName, this._object, 'all').then(variables => { if (this._this) { return session._createVariable(this._evalName, 'this', this._this).then(variable => { if (variable) { variables.push(variable); } return variables; }); } else { return variables; } }); } } SetValue(session, name, value) { return session._setPropertyValue(this._object.handle, name, value); } } exports.PropertyContainer = PropertyContainer; class SetMapContainer { constructor(evalName, obj) { this._evalName = evalName; this._object = obj; } Expand(session, filter, start, count) { if (filter === 'named') { return session._createSetMapProperties(this._evalName, this._object); } if (this._object.type === 'set') { return session._createSetElements(this._object, start, count); } else { return session._createMapElements(this._object, start, count); } } SetValue(session, name, value) { return Promise.reject(new Error(Expander.SET_VALUE_ERROR)); } } exports.SetMapContainer = SetMapContainer; class ScopeContainer { constructor(scope, obj, ths) { this._frame = scope.frameIndex; this._scope = scope.index; this._object = obj; this._this = ths; } Expand(session, filter, start, count) { return session._createProperties('', this._object, filter).then(variables => { if (this._this) { return session._createVariable('', 'this', this._this).then(variable => { if (variable) { variables.push(variable); } return variables; }); } else { return variables; } }); } SetValue(session, name, value) { return session._setVariableValue(this._frame, this._scope, name, value); } } exports.ScopeContainer = ScopeContainer; class Script { constructor(script) { this.contents = script.source; } } class InternalSourceBreakpoint { constructor(line, column = 0, condition, hitter) { this.line = this.orgLine = line; this.column = this.orgColumn = column; this.condition = condition; this.hitCount = 0; this.hitter = hitter; } } /** * A SourceSource represents the source contents of an internal module or of a source map with inlined contents. */ class SourceSource { constructor(sid, content) { this.scriptId = sid; this.source = content; } } class NodeDebugSession extends vscode_debugadapter_1.LoggingDebugSession { constructor() { super('node-debug.txt'); this._traceAll = false; // options this._tryToInjectExtension = true; this._skipRejects = false; // do not stop on rejected promises this._maxVariablesPerScope = 100; // only load this many variables for a scope this._smartStep = false; // try to automatically step over uninteresting source this._mapToFilesOnDisk = true; // by default try to map node.js scripts to files on disk this._compareContents = true; // by default verify that script contents is same as file contents this._supportsRunInTerminalRequest = false; this._nodeProcessId = -1; // pid of the node runtime this._functionBreakpoints = new Array(); // node function breakpoint ids this._scripts = new Map(); // script cache this._files = new Map(); // file cache this._scriptId2Handle = new Map(); this._inlinedContentHandle = new Map(); this._modifiedSources = new Set(); // track edited files this._hitCounts = new Map(); // breakpoint ID -> ignore count // session configurations this._noDebug = false; this._attachMode = false; this._restartMode = false; this._console = 'internalConsole'; this._stepBack = false; // state valid between stop events this._variableHandles = new vscode_debugadapter_1.Handles(); this._frameHandles = new vscode_debugadapter_1.Handles(); this._sourceHandles = new vscode_debugadapter_1.Handles(); this._refCache = new Map(); this._pollForNodeProcess = false; this._nodeInjectionAvailable = false; this._gotDebuggerEvent = false; this._smartStepCount = 0; this._catchRejects = false; this._disableSkipFiles = false; // this debugger uses zero-based lines and columns which is the default // so the following two calls are not really necessary. this.setDebuggerLinesStartAt1(false); this.setDebuggerColumnsStartAt1(false); this._node = new nodeV8Protocol_1.NodeV8Protocol(response => { // if request successful, cache alls refs if (response.success && response.refs) { const oldSize = this._refCache.size; for (let r of response.refs) { this._cache(r.handle, r); } if (this._refCache.size !== oldSize) { this.log('rc', `NodeV8Protocol hook: ref cache size: ${this._refCache.size}`); } } }); this._node.on('break', (event) => { this._stopped('break'); this._handleNodeBreakEvent(event.body); }); this._node.on('exception', (event) => { this._stopped('exception'); this._handleNodeExceptionEvent(event.body); }); /* this._node.on('beforeCompile', (event: NodeV8Event) => { //this.outLine(`beforeCompile ${this._scriptToPath(event.body.script)}`); this.sendEvent(new Event('customScriptLoad', { script: this._scriptToPath(event.body.script) })); }); */ this._node.on('afterCompile', (event) => { //this.outLine(`afterCompile ${this._scriptToPath(event.body.script)}`); this.sendEvent(new vscode_debugadapter_1.LoadedSourceEvent('new', this._scriptToSource(event.body.script))); //this.sendEvent(new Event('scriptLoaded', { path: this._scriptToPath(event.body.script) })); }); this._node.on('close', (event) => { this._terminated('node v8protocol close'); }); this._node.on('error', (event) => { this._terminated('node v8protocol error'); }); /* this._node.on('diagnostic', (event: NodeV8Event) => { this.outLine(`diagnostic event ${event.body.reason}`); }); */ } /** * Analyse why node has stopped and sends StoppedEvent if necessary. */ _handleNodeExceptionEvent(eventBody) { // should we skip this location? if (this._skip(eventBody)) { this._node.command('continue'); return; } let description; // in order to identify rejects extract source at current location if (eventBody.sourceLineText && typeof eventBody.sourceColumn === 'number') { let source = eventBody.sourceLineText.substr(eventBody.sourceColumn); if (source.indexOf('reject(') === 0) { if (this._skipRejects && !this._catchRejects) { this._node.command('continue'); return; } description = localize(1, null); if (eventBody.exception.text) { eventBody.exception.text = localize(2, null, eventBody.exception.text); } else { eventBody.exception.text = localize(3, null); } } } // send event this._exception = eventBody; this._sendStoppedEvent('exception', description, eventBody.exception.text); } /** * Analyse why node has stopped and sends StoppedEvent if necessary. */ _handleNodeBreakEvent(eventBody) { const breakpoints = eventBody.breakpoints; // check for breakpoints if (Array.isArray(breakpoints) && breakpoints.length > 0) { this._disableSkipFiles = this._skip(eventBody); const id = breakpoints[0]; if (!this._gotEntryEvent && id === 1) { this.log('la', '_handleNodeBreakEvent: suppressed stop-on-entry event'); // do not send event now this._rememberEntryLocation(eventBody.script.name, eventBody.sourceLine, eventBody.sourceColumn); return; } this._sendBreakpointStoppedEvent(id); return; } // in order to identify debugger statements extract source at current location if (eventBody.sourceLineText && typeof eventBody.sourceColumn === 'number') { let source = eventBody.sourceLineText.substr(eventBody.sourceColumn); if (source.indexOf('debugger') === 0) { this._gotDebuggerEvent = true; this._sendStoppedEvent('debugger_statement'); return; } } // must be the result of a 'step' let reason = 'step'; if (this._restartFramePending) { this._restartFramePending = false; reason = 'frame_entry'; } if (!this._disableSkipFiles) { // should we continue until we find a better place to stop? if ((this._smartStep && this._sourceMaps) || this._skipFiles) { this._skipGenerated(eventBody).then(r => { if (r) { this._node.command('continue', { stepaction: 'in' }); this._smartStepCount++; } else { this._sendStoppedEvent(reason); } }); return; } } this._sendStoppedEvent(reason); } _sendBreakpointStoppedEvent(breakpointId) { // evaluate hit counts let ibp = this._hitCounts.get(breakpointId); if (ibp) { ibp.hitCount++; if (ibp.hitter && !ibp.hitter(ibp.hitCount)) { this._node.command('continue'); return; } } this._sendStoppedEvent('breakpoint'); } _sendStoppedEvent(reason, description, exception_text) { if (this._smartStepCount > 0) { this.log('ss', `_handleNodeBreakEvent: ${this._smartStepCount} steps skipped`); this._smartStepCount = 0; } const e = new vscode_debugadapter_1.StoppedEvent(reason, NodeDebugSession.DUMMY_THREAD_ID, exception_text); if (!description) { switch (reason) { case 'step': description = localize(4, null); break; case 'breakpoint': description = localize(5, null); break; case 'exception': description = localize(6, null); break; case 'pause': description = localize(7, null); break; case 'entry': description = localize(8, null); break; case 'debugger_statement': description = localize(9, null); break; case 'frame_entry': description = localize(10, null); break; } } e.body.description = description; this.sendEvent(e); } isSkipped(path) { return this._skipFiles ? PathUtils.multiGlobMatches(this._skipFiles, path) : false; } /** * Returns true if a source location of the given event should be skipped. */ _skip(event) { if (this._skipFiles) { let path = this._scriptToPath(event.script); // if launch.json defines localRoot and remoteRoot try to convert remote path back to a local path let localPath = this._remoteToLocal(path); return PathUtils.multiGlobMatches(this._skipFiles, localPath); } return false; } /** * Returns true if a source location of the given event should be skipped. */ _skipGenerated(event) { let path = this._scriptToPath(event.script); // if launch.json defines localRoot and remoteRoot try to convert remote path back to a local path let localPath = this._remoteToLocal(path); if (this._skipFiles) { if (PathUtils.multiGlobMatches(this._skipFiles, localPath)) { return Promise.resolve(true); } return Promise.resolve(false); } if (this._smartStep) { // try to map let line = event.sourceLine; let column = this._adjustColumn(line, event.sourceColumn); return this._sourceMaps.MapToSource(localPath, null, line, column).then(mapresult => { return !mapresult; }); } return Promise.resolve(false); } toggleSkippingResource(response, resource) { resource = decodeURI(URL.parse(resource).pathname); if (this.isSkipped(resource)) { if (!this._skipFiles) { this._skipFiles = new Array(); } this._skipFiles.push('!' + resource); } else { if (!this._skipFiles) { this._skipFiles = new Array(); } this._skipFiles.push(resource); } this.sendResponse(response); } /** * create a path for a script following these rules: * - script name is an absolute path: return name as is * - script name is an internal module: return "<node_internals/name" * - script has no name: return "<node_internals/VMnnn" where nnn is the script ID */ _scriptToPath(script) { let name = script.name; if (name) { if (PathUtils.isAbsolutePath(name)) { return name; } } else { name = `VM${script.id}`; } return `${NodeDebugSession.NODE_INTERNALS}/${name}`; } /** * create a Source for a script following these rules: * - script name is an absolute path: return name as is * - script name is an internal module: return "<node_internals/name" * - script has no name: return "<node_internals/VMnnn" where nnn is the script ID */ _scriptToSource(script) { let path = script.name; if (path) { if (!PathUtils.isAbsolutePath(path)) { path = `${NodeDebugSession.NODE_INTERNALS}/${path}`; } } else { path = `${NodeDebugSession.NODE_INTERNALS}/VM${script.id}`; } return new vscode_debugadapter_1.Source(Path.basename(path), path, this._getScriptIdHandle(script.id)); } /** * Special treatment for internal modules: * we remove the '<node_internals>/' or '<node_internals>\' prefix and return either the name of the module or its ID */ _pathToScript(path) { const result = NodeDebugSession.NODE_INTERNALS_VM.exec(path); if (result && result.length >= 2) { return +result[1]; } return path.replace(NodeDebugSession.NODE_INTERNALS_PREFIX, ''); } /** * clear everything that is no longer valid after a new stopped event. */ _stopped(reason) { this._stoppedReason = reason; this.log('la', `_stopped: got ${reason} event from node`); this._exception = undefined; this._variableHandles.reset(); this._frameHandles.reset(); this._refCache = new Map(); this.log('rc', `_stopped: new ref cache`); } /** * The debug session has terminated. */ _terminated(reason) { this.log('la', `_terminated: ${reason}`); if (!this._isTerminated) { this._isTerminated = true; if (this._restartMode && this._attachSuccessful && !this._inShutdown) { this.sendEvent(new vscode_debugadapter_1.TerminatedEvent({ port: this._port })); } else { this.sendEvent(new vscode_debugadapter_1.TerminatedEvent()); } } } //---- initialize request ------------------------------------------------------------------------------------------------- initializeRequest(response, args) { this.log('la', `initializeRequest: adapterID: ${args.adapterID}`); this._adapterID = args.adapterID; if (args.locale) { localize = nls.config({ locale: args.locale })(__filename); } if (typeof args.supportsRunInTerminalRequest === 'boolean') { this._supportsRunInTerminalRequest = args.supportsRunInTerminalRequest; } //---- Send back feature and their options response.body = response.body || {}; // This debug adapter supports the configurationDoneRequest. response.body.supportsConfigurationDoneRequest = true; // This debug adapter supports function breakpoints. response.body.supportsFunctionBreakpoints = true; // This debug adapter supports conditional breakpoints. response.body.supportsConditionalBreakpoints = true; // This debug adapter does not support a side effect free evaluate request for data hovers. response.body.supportsEvaluateForHovers = false; // This debug adapter supports two exception breakpoint filters response.body.exceptionBreakpointFilters = [ { label: localize(11, null), filter: 'all', default: false }, { label: localize(12, null), filter: 'uncaught', default: true } ]; if (this._skipRejects) { response.body.exceptionBreakpointFilters.push({ label: localize(13, null), filter: 'rejects', default: false }); } // This debug adapter supports setting variables response.body.supportsSetVariable = true; // This debug adapter supports the restartFrame request response.body.supportsRestartFrame = true; // This debug adapter supports the completions request response.body.supportsCompletionsRequest = true; // This debug adapter supports the exception info request response.body.supportsExceptionInfoRequest = true; // This debug adapter supports delayed loading of stackframes response.body.supportsDelayedStackTraceLoading = true; this.sendResponse(response); } //---- launch request ----------------------------------------------------------------------------------------------------- launchRequest(response, args) { if (this._processCommonArgs(response, args)) { return; } if (args.__restart && typeof args.__restart.port === 'number') { this._attach(response, args, args.__restart.port, undefined, args.timeout); return; } this._noDebug = (typeof args.noDebug === 'boolean') && args.noDebug; if (typeof args.console === 'string') { switch (args.console) { case 'internalConsole': case 'integratedTerminal': case 'externalTerminal': this._console = args.console; break; default: this.sendErrorResponse(response, 2028, localize(14, null, args.console)); return; } } else if (typeof args.externalConsole === 'boolean' && args.externalConsole) { this._console = 'externalTerminal'; } if (args.useWSL && !WSL.subsystemLinuxPresent()) { this.sendErrorResponse(response, 2007, localize(15, null)); return; } const port = args.port || random(3000, 50000); let runtimeExecutable = args.runtimeExecutable; if (args.useWSL) { runtimeExecutable = runtimeExecutable || NodeDebugSession.NODE; } else if (runtimeExecutable) { if (!Path.isAbsolute(runtimeExecutable)) { const re = PathUtils.findOnPath(runtimeExecutable); if (!re) { this.sendErrorResponse(response, 2001, localize(16, null, '{_runtime}'), { _runtime: runtimeExecutable }); return; } runtimeExecutable = re; } else { const re = PathUtils.findExecutable(runtimeExecutable); if (!re) { this.sendNotExistErrorResponse(response, 'runtimeExecutable', runtimeExecutable); return; } runtimeExecutable = re; } } else { const re = PathUtils.findOnPath(NodeDebugSession.NODE); if (!re) { this.sendErrorResponse(response, 2001, localize(17, null, '{_runtime}'), { _runtime: NodeDebugSession.NODE }); return; } runtimeExecutable = re; } let runtimeArgs = args.runtimeArgs || []; const programArgs = args.args || []; let programPath = args.program; if (programPath) { if (!Path.isAbsolute(programPath)) { this.sendRelativePathErrorResponse(response, 'program', programPath); return; } if (!FS.existsSync(programPath)) { if (!FS.existsSync(programPath + '.js')) { this.sendNotExistErrorResponse(response, 'program', programPath); return; } programPath += '.js'; } programPath = Path.normalize(programPath); if (PathUtils.normalizeDriveLetter(programPath) !== PathUtils.realPath(programPath)) { this.outLine(localize(18, null)); } } if (!args.runtimeArgs && !this._noDebug) { runtimeArgs = ['--nolazy']; } if (programPath) { if (NodeDebugSession.isJavaScript(programPath)) { if (this._sourceMaps) { // if programPath is a JavaScript file and sourceMaps are enabled, we don't know whether // programPath is the generated file or whether it is the source (and we need source mapping). // Typically this happens if a tool like 'babel' or 'uglify' is used (because they both transpile js to js). // We use the source maps to find a 'source' file for the given js file. this._sourceMaps.MapPathFromSource(programPath).then(generatedPath => { if (generatedPath && generatedPath !== programPath) { // programPath must be source because there seems to be a generated file for it this.log('sm', `launchRequest: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`); programPath = generatedPath; } else { this.log('sm', `launchRequest: program '${programPath}' seems to be the generated file`); } this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs, port); }); return; } } else { // node cannot execute the program directly if (!this._sourceMaps) { this.sendErrorResponse(response, 2002, localize(19, null, '{path}'), { path: programPath }); return; } this._sourceMaps.MapPathFromSource(programPath).then(generatedPath => { if (!generatedPath) { if (args.outFiles || args.outDir) { this.sendErrorResponse(response, 2009, localize(20, null, '{path}'), { path: programPath }); } else { this.sendErrorResponse(response, 2003, localize(21, null, '{path}', 'outFiles'), { path: programPath }); } return; } this.log('sm', `launchRequest: program '${programPath}' seems to be the source; launch the generated file '${generatedPath}' instead`); programPath = generatedPath; this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs, port); }); return; } } this.launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs, port); } launchRequest2(response, args, programPath, programArgs, runtimeExecutable, runtimeArgs, port) { let program; let workingDirectory = args.cwd; if (workingDirectory) { if (!Path.isAbsolute(workingDirectory)) { this.sendRelativePathErrorResponse(response, 'cwd', workingDirectory); return; } if (!FS.existsSync(workingDirectory)) { this.sendNotExistErrorResponse(response, 'cwd', workingDirectory); return; } // if working dir is given and if the executable is within that folder, we make the executable path relative to the working dir if (programPath) { program = Path.relative(workingDirectory, programPath); } } else if (programPath) { // if no working dir given, we use the direct folder of the executable workingDirectory = Path.dirname(programPath); program = Path.basename(programPath); } // figure out when to add a '--debug-brk=nnnn' let launchArgs = [runtimeExecutable]; if (!this._noDebug) { if (args.port) { // only if the default runtime 'node' is used without arguments if (!args.runtimeExecutable && !args.runtimeArgs) { // use the specfied port launchArgs.push(`--debug-brk=${port}`); } } else { // use a random port launchArgs.push(`--debug-brk=${port}`); } } launchArgs = launchArgs.concat(runtimeArgs); if (program) { launchArgs.push(program); } launchArgs = launchArgs.concat(programArgs); const address = args.address; const timeout = args.timeout; let envVars = args.env; // read env from disk and merge into envVars if (args.envFile) { try { const buffer = PathUtils.stripBOM(FS.readFileSync(args.envFile, 'utf8')); const env = {}; buffer.split('\n').forEach(line => { const r = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/); if (r !== null) { const key = r[1]; if (!process.env[key]) { let value = r[2] || ''; if (value.length > 0 && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') { value = value.replace(/\\n/gm, '\n'); } env[key] = value.replace(/(^['"]|['"]$)/g, ''); } } }); envVars = PathUtils.extendObject(env, args.env); // launch config env vars overwrite .env vars } catch (e) { this.sendErrorResponse(response, 2029, localize(22, null, '{_error}'), { _error: e.message }); return; } } const wslLaunchArgs = WSL.createLaunchArg(args.useWSL, this._supportsRunInTerminalRequest && this._console === 'externalTerminal', workingDirectory, launchArgs[0], launchArgs.slice(1), program); // workaround for #35249 // if using subsystem linux, we use local/remote mapping (if not configured by user) if (args.useWSL && !args.localRoot && !args.remoteRoot) { this._localRoot = wslLaunchArgs.localRoot; this._remoteRoot = wslLaunchArgs.remoteRoot; } if (this._supportsRunInTerminalRequest && (this._console === 'externalTerminal' || this._console === 'integratedTerminal')) { const termArgs = { kind: this._console === 'integratedTerminal' ? 'integrated' : 'external', title: localize(23, null), cwd: wslLaunchArgs.cwd, args: wslLaunchArgs.combined, env: envVars }; this.runInTerminalRequest(termArgs, NodeDebugSession.RUNINTERMINAL_TIMEOUT, runResponse => { if (runResponse.success) { // since node starts in a terminal, we cannot track it with an 'exit' handler // plan for polling after we have gotten the process pid. this._pollForNodeProcess = !args.runtimeExecutable // only if no 'runtimeExecutable' is specified && !args.useWSL; // it will not work with WSL either if (this._noDebug) { this.sendResponse(response); } else { this._attach(response, args, port, address, timeout); } } else { this.sendErrorResponse(response, 2011, localize(24, null, '{_error}'), { _error: runResponse.message }); this._terminated('terminal error: ' + runResponse.message); } }); } else { this._sendLaunchCommandToConsole(launchArgs); // merge environment variables into a copy of the process.env envVars = PathUtils.extendObject(PathUtils.extendObject({}, process.env), envVars); const options = { cwd: workingDirectory, env: envVars }; const nodeProcess = CP.spawn(wslLaunchArgs.executable, wslLaunchArgs.args, options); nodeProcess.on('error', (error) => { // tslint:disable-next-line:no-bitwise this.sendErrorResponse(response, 2017, localize(25, null, '{_error}'), { _error: error.message }, vscode_debugadapter_1.ErrorDestination.Telemetry | vscode_debugadapter_1.ErrorDestination.User); this._terminated(`failed to launch target (${error})`); }); nodeProcess.on('exit', () => { this._terminated('target exited'); }); nodeProcess.on('close', (code) => { this._terminated('target closed'); }); this._nodeProcessId = nodeProcess.pid; this._captureOutput(nodeProcess); if (this._noDebug) { this.sendResponse(response); } else { this._attach(response, args, port, address, timeout); } } } _sendLaunchCommandToConsole(args) { // print the command to launch the target to the debug console let cli = ''; for (let a of args) { if (a.indexOf(' ') >= 0) { cli += '\'' + a + '\''; } else { cli += a; } cli += ' '; } this.outLine(cli); } _captureOutput(process) { process.stdout.on('data', (data) => { this.sendEvent(new vscode_debugadapter_1.OutputEvent(data.toString(), 'stdout')); }); process.stderr.on('data', (data) => { this.sendEvent(new vscode_debugadapter_1.OutputEvent(data.toString(), 'stderr')); }); } /** * returns true on error. */ _processCommonArgs(response, args) { let stopLogging = true; if (typeof args.trace === 'boolean') { this._trace = args.trace ? ['all'] : undefined; this._traceAll = args.trace; } else if (typeof args.trace === 'string') { this._trace = args.trace.split(','); this._traceAll = this._trace.indexOf('all') >= 0; if (this._trace.indexOf('dap') >= 0) { vscode_debugadapter_1.logger.setup(vscode_debugadapter_1.Logger.LogLevel.Verbose, /*logToFile=*/ false); stopLogging = false; } } if (stopLogging) { vscode_debugadapter_1.logger.setup(vscode_debugadapter_1.Logger.LogLevel.Stop, false); } if (typeof args.stepBack === 'boolean') { this._stepBack = args.stepBack; } if (typeof args.mapToFilesOnDisk === 'boolean') { this._mapToFilesOnDisk = args.mapToFilesOnDisk; } if (typeof args.smartStep === 'boolean') { this._smartStep = args.smartStep; } if (typeof args.skipFiles) { this._skipFiles = args.skipFiles; } if (typeof args.stopOnEntry === 'boolean') { this._stopOnEntry = args.stopOnEntry; } if (typeof args.restart === 'boolean') { this._restartMode = args.restart; } if (args.localRoot) { const localRoot = args.localRoot; if (!Path.isAbsolute(localRoot)) { this.sendRelativePathErrorResponse(response, 'localRoot', localRoot); return true; } if (!FS.existsSync(localRoot)) { this.sendNotExistErrorResponse(response, 'localRoot', localRoot); return true; } this._localRoot = localRoot; } this._remoteRoot = args.remoteRoot; if (!this._sourceMaps) { if (args.sourceMaps === undefined) { args.sourceMaps = true; } if (typeof args.sourceMaps === 'boolean' && args.sourceMaps) { const generatedCodeDirectory = args.outDir; if (generatedCodeDirectory) { if (!Path.isAbsolute(generatedCodeDirectory)) { this.sendRelativePathErrorResponse(response, 'outDir', generatedCodeDirectory); return true; } if (!FS.existsSync(generatedCodeDirectory)) { this.sendNotExistErrorResponse(response, 'outDir', generatedCodeDirectory); return true; } } this._sourceMaps = new sourceMaps_1.SourceMaps(this, generatedCodeDirectory, args.outFiles); } } return false; } //---- attach request ----------------------------------------------------------------------------------------------------- attachRequest(response, args) { if (this._processCommonArgs(response, args)) { return; } this._attachMode = true; this._attach(response, args, args.port, args.address, args.timeout); } /* * shared 'attach' code used in launchRequest and attachRequest. */ _attach(response, args, port, address, timeout) { if (!port) { port = 5858; } this._port = port; if (!address || address === 'localhost') { address = '127.0.0.1'; } if (!timeout) { timeout = NodeDebugSession.ATTACH_TIMEOUT; } this.log('la', `_attach: address: ${address} port: ${port}`); let connected = false; const socket = new Net.Socket(); socket.connect(port, address); socket.on('connect', err => { this.log('la', '_attach: connected'); connected = true; this._node.startDispatch(socket, socket); this._isRunning().then(running => { if (this._pollForNodeProcess) { this._pollForNodeTermination(); } setTimeout(() => { this._injectDebuggerExtensions().then(_ => { if (!this._stepBack) { // does runtime support 'step back'? const v = this._node.embeddedHostVersion; // x.y.z version represented as (x*100+y)*100+z if (!this._node.v8Version && v >= 70000) { this._stepBack = true; } } if (this._stepBack) { response.body = { supportsStepBack: true }; } this.sendResponse(response); this._startInitialize(!running); }); }, 10); }).catch(resp => { this._sendNodeResponse(response, resp); }); }); const endTime = new Date().getTime() + timeout; socket.on('error', err => { if (connected) { // since we are connected this error is fatal this.sendErrorResponse(response, 2010, localize(26, null, '{_error}'), { _error: err.message }); } else { // we are not yet connected so retry a few times if (err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') { const now = new Date().getTime(); if (now < endTime) { setTimeout(() => { this.log('la', '_attach: retry socket.connect'); socket.connect(port, address); }, 200); // retry after 200 ms } else { if (typeof args.port === 'number') { this.sendErrorResponse(response, 2033, localize(27, null)); } else { this.sendErrorResponse(response, 2034, localize(28, null)); } } } else { this.sendErrorResponse(response, 2010, localize(29, null, '{_error}'), { _error: err.message }); } } }); socket.on('end', err => { this._terminated('socket end'); }); } /** * Determine whether the runtime is running or stopped. * We do this by running an 'evaluate' request * (a benevolent side effect of the evaluate is to find the process id and runtime version). */ _isRunning() { return new Promise((completeDispatch, errorDispatch) => { this._isRunningWithRetry(0, completeDispatch, errorDispatch); }); } _isRunningWithRetry(retryCount, completeDispatch, errorDispatch) { this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp) => { if (resp.success && resp.body.value !== undefined) { this._nodeProcessId = +resp.body.value; this.log('la', `__initialize: got process id ${this._nodeProcessId} from node`); this.logNodeVersion(); } else { if (resp.message.indexOf('process is not defined') >= 0) { this.log('la', '__initialize: process not defined error; got no pid'); resp.success = true; // continue and try to get process.pid later } } if (resp.success) { completeDispatch(resp.running); } else { this.log('la', '__initialize: retrieving process id from node failed'); if (retryCount < 4) { setTimeout(() => { // recurse this._isRunningWithRetry(retryCount + 1, completeDispatch, errorDispatch); }, 100); return; } else { errorDispatch(resp); } } }); } logNodeVersion() { this._node.command('evaluate', { expression: 'process.version', global: true }, (resp) => { if (resp.success && resp.body.value !== undefined) { const version = resp.body.value; /* __GDPR__ "nodeVersion" : { "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } */ this.sendEvent(new vscode_debugadapter_1.OutputEvent('nodeVersion', 'telemetry', { version })); this.log('la', `_initialize: target node version: ${version}`); } }); } _pollForNodeTermination() { const id = setInterval(() => { try { if (this._nodeProcessId > 0) { process.kill(this._nodeProcessId, 0); // node.d.ts doesn't like number argumnent } else { clearInterval(id); } } catch (e) { clearInterval(id); this._terminated('node process kill exception'); } }, NodeDebugSession.NODE_TERMINATION_POLL_INTERVAL); } /* * Inject code into node.js to address slowness issues when inspecting large data structures. */ _injectDebuggerExtensions() { if (this._tryToInjectExtension) { const v = this._node.embeddedHostVersion; // x.y.z version represented as (x*100+y)*100+z if (this._node.v8Version && ((v >= 1200 && v < 10000) || (v >= 40301 && v < 50000) || (v >= 50600))) { try { const contents = FS.readFileSync(Path.join(__dirname, NodeDebugSession.DEBUG_INJECTION), 'utf8'); const args = { expression: contents, global: true, disable_break: true }; return this._node.evaluate(args).then(resp => { this.log('la', `_injectDebuggerExtensions: code injection successful`); this._nodeInjectionAvailable = true; return true; }).catch(resp => { this.log('la', `_injectDebuggerExtensions: code injection failed with error '${resp.message}'`); return true; }); } catch (e) { // fall through } } } return Promise.resolve(true); } /* * start the initialization sequence: * 1. wait for 'break-on-entry' (with timeout) * 2. send 'inititialized' event in order to trigger setBreakpointEvents request from client * 3. prepare for sending 'break-on-entry' or 'continue' later in configurationDoneRequest() */ _startInitialize(stopped, n = 0) { if (n === 0) { this.log('la', `_startInitialize: stopped: ${stopped}`); } // wait at most 500ms for receiving the break on entry event // (since in attach mode we cannot enforce that node is started with --debug-brk, we cannot assume that we receive this event) if (!this._gotEntryEvent && n < 10) { setTimeout(() => { // recurse this._startInitialize(stopped, n + 1); }, 50); return; } if (this._gotEntryEvent) { this.log('la', `_startInitialize: got break on entry event after ${n} retries`); if (this._nodeProcessId <= 0) { // if we haven't gotten a process pid so far, we try it again this._node.command('evaluate', { expression: 'process.pid', global: true }, (resp) => { if (resp.success && resp.body.value !== undefined) { this._nodeProcessId = +resp.body.value; this.log('la', `_initialize: got process id ${this._nodeProcessId} from node (2nd try)`); this.logNodeVersion(); } this._startInitialize2(stopped); }); } else { this._startInitialize2(stopped); } } else { this.log('la', `_startInitialize: no entry event after ${n} retries; giving up`); this._gotEntryEvent = true; // we pretend to got one so that no 'entry' event will show up later... this._node.command('frame', null, (resp) => { if (resp.success) { const s = this._getValueFromCache(resp.body.script); this._rememberEntryLocation(s.name, resp.body.line, resp.body.column); } this._startInitialize2(stopped); }); } } _startInitialize2(stopped) { // request UI to send breakpoints this.log('la', '_startInitialize2: fire initialized event'); this.sendEvent(new vscode_debugadapter_1.InitializedEvent()); this._attachSuccessful = true; // in attach-mode we don't know whether the debuggee has been launched in 'stop on entry' mode // so we use the stopped state of the VM if (this._attachMode) { this.log('la', `_startInitialize2: in attach mode we guess stopOnEntry flag to be '${stopped}''`); this._stopOnEntry = stopped; } if (this._stopOnEntry) { // user has requested 'stop on entry' so send out a stop-on-entry event this.log('la', '_startInitialize2: fire stop-on-entry event'); this._sendStoppedEvent('entry'); } else { // since we are stopped but UI doesn't know about this, remember that we later do the right thing in configurationDoneRequest() if (this._gotDebuggerEvent) { this._needDebuggerEvent = true; } else { this.log('la', `_startInitialize2: remember to do a 'Continue' later`); this._needContinue = true; } } } //---- disconnect request ------------------------------------------------------------------------------------------------- disconnectRequest(response, args) { this.shutdown(); this.log('la', 'disconnectRequest: send response')