diff --git a/.eslintrc b/.eslintrc index 981a795..337ff4f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,145 @@ -{ - "extends": "groupon-node6" -} +env: + node: true + es6: true + +parserOptions: + ecmaVersion: 2016 + +rules: + # Possible Errors + # http://eslint.org/docs/rules/#possible-errors + comma-dangle: [2, only-multiline] + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: [2, functions] + no-extra-semi: 2 + no-func-assign: 2 + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-obj-calls: 2 + no-proto: 2 + no-template-curly-in-string: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + no-unsafe-negation: 2 + use-isnan: 2 + valid-typeof: 2 + + # Best Practices + # http://eslint.org/docs/rules/#best-practices + dot-location: [2, property] + no-fallthrough: 2 + no-global-assign: 2 + no-multi-spaces: 2 + no-octal: 2 + no-redeclare: 2 + no-self-assign: 2 + no-unused-labels: 2 + no-useless-call: 2 + no-useless-escape: 2 + no-void: 2 + no-with: 2 + + # Strict Mode + # http://eslint.org/docs/rules/#strict-mode + strict: [2, global] + + # Variables + # http://eslint.org/docs/rules/#variables + no-delete-var: 2 + no-undef: 2 + no-unused-vars: [2, {args: none}] + + # Node.js and CommonJS + # http://eslint.org/docs/rules/#nodejs-and-commonjs + no-mixed-requires: 2 + no-new-require: 2 + no-path-concat: 2 + no-restricted-modules: [2, sys, _linklist] + no-restricted-properties: [2, { + object: assert, + property: deepEqual, + message: Please use assert.deepStrictEqual(). + }, { + property: __defineGetter__, + message: __defineGetter__ is deprecated. + }, { + property: __defineSetter__, + message: __defineSetter__ is deprecated. + }] + + # Stylistic Issues + # http://eslint.org/docs/rules/#stylistic-issues + brace-style: [2, 1tbs, {allowSingleLine: true}] + comma-spacing: 2 + comma-style: 2 + computed-property-spacing: 2 + eol-last: 2 + func-call-spacing: 2 + func-name-matching: 2 + indent: [2, 2, {SwitchCase: 1, MemberExpression: 1}] + key-spacing: [2, {mode: minimum}] + keyword-spacing: 2 + linebreak-style: [2, unix] + max-len: [2, 80, 2] + new-parens: 2 + no-mixed-spaces-and-tabs: 2 + no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] + no-tabs: 2 + no-trailing-spaces: 2 + quotes: [2, single, avoid-escape] + semi: 2 + semi-spacing: 2 + space-before-blocks: [2, always] + space-before-function-paren: [2, never] + space-in-parens: [2, never] + space-infix-ops: 2 + space-unary-ops: 2 + + # ECMAScript 6 + # http://eslint.org/docs/rules/#ecmascript-6 + arrow-parens: [2, always] + arrow-spacing: [2, {before: true, after: true}] + constructor-super: 2 + no-class-assign: 2 + no-confusing-arrow: 2 + no-const-assign: 2 + no-dupe-class-members: 2 + no-new-symbol: 2 + no-this-before-super: 2 + prefer-const: [2, {ignoreReadBeforeAssign: true}] + rest-spread-spacing: 2 + template-curly-spacing: 2 + + # Custom rules in tools/eslint-rules + align-function-arguments: 2 + align-multiline-assignment: 2 + assert-fail-single-argument: 2 + new-with-error: [2, Error, RangeError, TypeError, SyntaxError, ReferenceError] + +# Global scoped method and vars +globals: + COUNTER_HTTP_CLIENT_REQUEST: false + COUNTER_HTTP_CLIENT_RESPONSE: false + COUNTER_HTTP_SERVER_REQUEST: false + COUNTER_HTTP_SERVER_RESPONSE: false + COUNTER_NET_SERVER_CONNECTION: false + COUNTER_NET_SERVER_CONNECTION_CLOSE: false + DTRACE_HTTP_CLIENT_REQUEST: false + DTRACE_HTTP_CLIENT_RESPONSE: false + DTRACE_HTTP_SERVER_REQUEST: false + DTRACE_HTTP_SERVER_RESPONSE: false + DTRACE_NET_SERVER_CONNECTION: false + DTRACE_NET_STREAM_END: false + LTTNG_HTTP_CLIENT_REQUEST: false + LTTNG_HTTP_CLIENT_RESPONSE: false + LTTNG_HTTP_SERVER_REQUEST: false + LTTNG_HTTP_SERVER_RESPONSE: false + LTTNG_NET_SERVER_CONNECTION: false + LTTNG_NET_STREAM_END: false diff --git a/lib/_inspect.js b/lib/_inspect.js new file mode 100644 index 0000000..1b8ec94 --- /dev/null +++ b/lib/_inspect.js @@ -0,0 +1,1548 @@ +/* + * Copyright Node.js contributors. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +'use strict'; +const Buffer = require('buffer').Buffer; +const { spawn } = require('child_process'); +const crypto = require('crypto'); +const { EventEmitter } = require('events'); +const http = require('http'); +const Path = require('path'); +const Repl = require('repl'); +const URL = require('url'); +const util = require('util'); +const vm = require('vm'); + +const debuglog = util.debuglog('inspect'); + +exports.port = 9229; + +const SHORTCUTS = { + cont: 'c', + next: 'n', + step: 's', + out: 'o', + backtrace: 'bt', + setBreakpoint: 'sb', + clearBreakpoint: 'cb', + run: 'r', +}; + +const HELP = ` +run, restart, r Run the application or reconnect +kill Kill a running application or disconnect + +cont, c Resume execution +next, n Continue to next line in current file +step, s Step into, potentially entering a function +out, o Step out, leaving the current function +backtrace, bt Print the current backtrace +list Print the source around the current line where execution + is currently paused + +setBreakpoint, sb Set a breakpoint +clearBreakpoint, cb Clear a breakpoint +breakpoints List all known breakpoints +breakOnException Pause execution whenever an exception is thrown +breakOnUncaught Pause execution whenever an exception isn't caught +breakOnNone Don't pause on exceptions (this is the default) + +watch(expr) Start watching the given expression +unwatch(expr) Stop watching an expression +watchers Print all watched expressions and their current values + +exec(expr) Evaluate the expression and print the value +repl Enter a debug repl that works like exec + +scripts List application scripts that are currently loaded +scripts(true) List all scripts (including node-internals) +`.trim(); + +const ProtocolClient = (function setupClient() { + const kOpCodeText = 0x1; + const kOpCodeClose = 0x8; + + const kFinalBit = 0x80; + const kReserved1Bit = 0x40; + const kReserved2Bit = 0x20; + const kReserved3Bit = 0x10; + const kOpCodeMask = 0xF; + const kMaskBit = 0x80; + const kPayloadLengthMask = 0x7F; + + const kMaxSingleBytePayloadLength = 125; + const kMaxTwoBytePayloadLength = 0xFFFF; + const kTwoBytePayloadLengthField = 126; + const kEightBytePayloadLengthField = 127; + const kMaskingKeyWidthInBytes = 4; + + function isEmpty(obj) { + return Object.keys(obj).length === 0; + } + + function unpackError({ code, message, data }) { + const err = new Error(`${message} - ${data}`); + err.code = code; + Error.captureStackTrace(err, unpackError); + return err; + } + + function encodeFrameHybi17(payload) { + var i; + + const dataLength = payload.length; + + let singleByteLength; + let additionalLength; + if (dataLength > kMaxTwoBytePayloadLength) { + singleByteLength = kEightBytePayloadLengthField; + additionalLength = Buffer.alloc(8); + let remaining = dataLength; + for (i = 0; i < 8; ++i) { + additionalLength[7 - i] = remaining & 0xFF; + remaining >>= 8; + } + } else if (dataLength > kMaxSingleBytePayloadLength) { + singleByteLength = kTwoBytePayloadLengthField; + additionalLength = Buffer.alloc(2); + additionalLength[0] = (dataLength & 0xFF00) >> 8; + additionalLength[1] = dataLength & 0xFF; + } else { + additionalLength = Buffer.alloc(0); + singleByteLength = dataLength; + } + + const header = Buffer.from([ + kFinalBit | kOpCodeText, + kMaskBit | singleByteLength, + ]); + + const mask = Buffer.alloc(4); + const masked = Buffer.alloc(dataLength); + for (i = 0; i < dataLength; ++i) { + masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; + } + + return Buffer.concat([header, additionalLength, mask, masked]); + } + + function decodeFrameHybi17(data) { + const dataAvailable = data.length; + const notComplete = { closed: false, payload: null, rest: data }; + let payloadOffset = 2; + if ((dataAvailable - payloadOffset) < 0) return notComplete; + + const firstByte = data[0]; + const secondByte = data[1]; + + const final = (firstByte & kFinalBit) !== 0; + const reserved1 = (firstByte & kReserved1Bit) !== 0; + const reserved2 = (firstByte & kReserved2Bit) !== 0; + const reserved3 = (firstByte & kReserved3Bit) !== 0; + const opCode = firstByte & kOpCodeMask; + const masked = (secondByte & kMaskBit) !== 0; + const compressed = reserved1; + if (compressed) { + throw new Error('Compressed frames not supported'); + } + if (!final || reserved2 || reserved3) { + throw new Error('Only compression extension is supported'); + } + + if (masked) { + throw new Error('Masked server frame - not supported'); + } + + let closed = false; + switch (opCode) { + case kOpCodeClose: + closed = true; + break; + case kOpCodeText: + break; + default: + throw new Error(`Unsupported op code ${opCode}`); + } + + let payloadLength = secondByte & kPayloadLengthMask; + switch (payloadLength) { + case kTwoBytePayloadLengthField: + payloadOffset += 2; + payloadLength = (data[2] << 8) + data[3]; + break; + + case kEightBytePayloadLengthField: + payloadOffset += 8; + payloadLength = 0; + for (var i = 0; i < 8; ++i) { + payloadLength <<= 8; + payloadLength |= data[2 + i]; + } + break; + + default: + // Nothing. We already have the right size. + } + if ((dataAvailable - payloadOffset - payloadLength) < 0) return notComplete; + + const payloadEnd = payloadOffset + payloadLength; + return { + payload: data.slice(payloadOffset, payloadEnd), + rest: data.slice(payloadEnd), + closed, + }; + } + + class Client extends EventEmitter { + constructor(port, host) { + super(); + this.handleChunk = this._handleChunk.bind(this); + + this._port = port; + this._host = host; + + this.reset(); + } + + _handleChunk(chunk) { + this._unprocessed = Buffer.concat([this._unprocessed, chunk]); + + while (this._unprocessed.length > 2) { + const { + closed, + payload: payloadBuffer, + rest + } = decodeFrameHybi17(this._unprocessed); + this._unprocessed = rest; + + if (closed) { + this.reset(); + return; + } + if (payloadBuffer === null) break; + + const payloadStr = payloadBuffer.toString(); + debuglog('< %s', payloadStr); + const lastChar = payloadStr[payloadStr.length - 1]; + if (payloadStr[0] !== '{' || lastChar !== '}') { + throw new Error(`Payload does not look like JSON: ${payloadStr}`); + } + let payload; + try { + payload = JSON.parse(payloadStr); + } catch (parseError) { + parseError.string = payloadStr; + throw parseError; + } + + const { id, method, params, result, error } = payload; + if (id) { + const handler = this._pending[id]; + if (handler) { + delete this._pending[id]; + handler(error, result); + } + } else if (method) { + this.emit('debugEvent', method, params); + this.emit(method, params); + } else { + throw new Error(`Unsupported response: ${payloadStr}`); + } + } + } + + reset() { + if (this._http) { + this._http.destroy(); + } + this._http = null; + this._lastId = 0; + this._socket = null; + this._pending = {}; + this._unprocessed = Buffer.alloc(0); + } + + callMethod(method, params) { + return new Promise((resolve, reject) => { + if (!this._socket) { + reject(new Error('Use `run` to start the app again.')); + return; + } + const data = { id: ++this._lastId, method, params }; + this._pending[data.id] = (error, result) => { + if (error) reject(unpackError(error)); + else resolve(isEmpty(result) ? undefined : result); + }; + const json = JSON.stringify(data); + debuglog('> %s', json); + this._socket.write(encodeFrameHybi17(Buffer.from(json))); + }); + } + + _fetchJSON(urlPath) { + return new Promise((resolve, reject) => { + const httpReq = http.get({ + host: this._host, + port: this._port, + path: urlPath, + }); + + const chunks = []; + + function onResponse(httpRes) { + function parseChunks() { + const resBody = Buffer.concat(chunks).toString(); + if (httpRes.statusCode !== 200) { + reject(new Error(`Unexpected ${httpRes.statusCode}: ${resBody}`)); + return; + } + try { + resolve(JSON.parse(resBody)); + } catch (parseError) { + reject(new Error(`Response didn't contain JSON: ${resBody}`)); + return; + } + } + + httpRes.on('error', reject); + httpRes.on('data', (chunk) => chunks.push(chunk)); + httpRes.on('end', parseChunks); + } + + httpReq.on('error', reject); + httpReq.on('response', onResponse); + }); + } + + connect() { + return this._discoverWebsocketPath() + .then((urlPath) => this._connectWebsocket(urlPath)); + } + + _discoverWebsocketPath() { + return this._fetchJSON('/json') + .then(([{ webSocketDebuggerUrl }]) => + URL.parse(webSocketDebuggerUrl).path); + } + + _connectWebsocket(urlPath) { + this.reset(); + + const key1 = crypto.randomBytes(16).toString('base64'); + debuglog('request websocket', key1); + + const httpReq = this._http = http.request({ + host: this._host, + port: this._port, + path: urlPath, + headers: { + Connection: 'Upgrade', + Upgrade: 'websocket', + 'Sec-WebSocket-Key': key1, + 'Sec-WebSocket-Version': '13', + }, + }); + httpReq.on('error', (e) => { + this.emit('error', e); + }); + httpReq.on('response', (httpRes) => { + if (httpRes.statusCode >= 400) { + process.stderr.write(`Unexpected HTTP code: ${httpRes.statusCode}\n`); + httpRes.pipe(process.stderr); + } else { + httpRes.pipe(process.stderr); + } + }); + + const handshakeListener = (res, socket) => { + // TODO: we *could* validate res.headers[sec-websocket-accept] + debuglog('websocket upgrade'); + + this._socket = socket; + socket.on('data', this.handleChunk); + socket.on('close', () => { + this.emit('close'); + }); + + Promise.all([ + this.callMethod('Runtime.enable'), + this.callMethod('Debugger.enable'), + this.callMethod('Debugger.setPauseOnExceptions', { state: 'none' }), + this.callMethod('Debugger.setAsyncCallStackDepth', { maxDepth: 0 }), + this.callMethod('Profiler.enable'), + this.callMethod('Profiler.setSamplingInterval', { interval: 100 }), + this.callMethod('Debugger.setBlackboxPatterns', { patterns: [] }), + this.callMethod('Runtime.runIfWaitingForDebugger'), + ]).then(() => { + this.emit('ready'); + }, (error) => { + this.emit('error', error); + }); + }; + + return new Promise((resolve, reject) => { + this.once('error', reject); + this.once('ready', resolve); + + httpReq.on('upgrade', handshakeListener); + httpReq.end(); + }); + } + } + + return Client; +}()); + +function createRepl(inspector) { + const NATIVES = process.binding('natives'); + + function isNativeUrl(url) { + return url.replace('.js', '') in NATIVES || url === 'bootstrap_node.js'; + } + + function getRelativePath(filename) { + const dir = `${Path.resolve()}/`; + + // Change path to relative, if possible + if (filename.indexOf(dir) === 0) { + return filename.slice(dir.length); + } + return filename; + } + + function toCallback(promise, callback) { + function forward(...args) { + process.nextTick(() => callback(...args)); + } + promise.then(forward.bind(null, null), forward); + } + + // Adds spaces and prefix to number + // maxN is a maximum number we should have space for + function leftPad(n, prefix, maxN) { + const s = n.toString(); + const nchars = Math.max(2, String(maxN).length) + 1; + const nspaces = nchars - s.length - 1; + + return prefix + ' '.repeat(nspaces) + s; + } + + function markSourceColumn(sourceText, position, useColors) { + if (!sourceText) return ''; + + const head = sourceText.slice(0, position); + let tail = sourceText.slice(position); + + // Colourize char if stdout supports colours + if (useColors) { + tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2'); + } + + // Return source line with coloured char at `position` + return [head, tail].join(''); + } + + function extractErrorMessage(stack) { + if (!stack) return ''; + const m = stack.match(/^\w+: ([^\n]+)/); + return m ? m[1] : stack; + } + + function convertResultToError(result) { + const { className, description } = result; + const err = new Error(extractErrorMessage(description)); + err.stack = description; + Object.defineProperty(err, 'name', { value: className }); + return err; + } + + const FUNCTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/; + + class RemoteObject { + constructor(attributes) { + Object.assign(this, attributes); + if (this.type === 'number') { + this.value = + this.unserializableValue ? +this.unserializableValue : +this.value; + } + } + + [util.inspect.custom](depth, opts) { + function formatProperty(prop) { + switch (prop.type) { + case 'string': + case 'undefined': + return util.inspect(prop.value, opts); + + case 'number': + case 'boolean': + return opts.stylize(prop.value, prop.type); + + case 'object': + case 'symbol': + if (prop.subtype === 'date') { + return util.inspect(new Date(prop.value), opts); + } + if (prop.subtype === 'array') { + return opts.stylize(prop.value, 'special'); + } + return opts.stylize(prop.value, prop.subtype || 'special'); + + default: + return prop.value; + } + } + switch (this.type) { + case 'boolean': + case 'number': + case 'string': + case 'undefined': + return util.inspect(this.value, opts); + + case 'symbol': + return opts.stylize(this.description, 'special'); + + case 'function': { + const fnNameMatch = this.description.match(FUNCTION_NAME_PATTERN); + const fnName = fnNameMatch ? `: ${fnNameMatch[1]}` : ''; + const formatted = `[${this.className}${fnName}]`; + return opts.stylize(formatted, 'special'); + } + + case 'object': + switch (this.subtype) { + case 'date': + return util.inspect(new Date(this.description), opts); + + case 'null': + return util.inspect(null, opts); + + case 'regexp': + return opts.stylize(this.description, 'regexp'); + + default: + break; + } + if (this.preview) { + const props = this.preview.properties + .map((prop, idx) => { + const value = formatProperty(prop); + if (prop.name === `${idx}`) return value; + return `${prop.name}: ${value}`; + }); + if (this.preview.overflow) { + props.push('...'); + } + const singleLine = props.join(', '); + const propString = + singleLine.length > 60 ? props.join(',\n ') : singleLine; + + return this.subtype === 'array' ? + `[ ${propString} ]` : `{ ${propString} }`; + } + return this.description; + + default: + return this.description; + } + } + } + + function convertResultToRemoteObject({ result, wasThrown }) { + if (wasThrown) return convertResultToError(result); + return new RemoteObject(result); + } + + function copyOwnProperties(target, source) { + Object.getOwnPropertyNames(source).forEach((prop) => { + const descriptor = Object.getOwnPropertyDescriptor(source, prop); + Object.defineProperty(target, prop, descriptor); + }); + } + + function aliasProperties(target, mapping) { + Object.keys(mapping).forEach((key) => { + const descriptor = Object.getOwnPropertyDescriptor(target, key); + Object.defineProperty(target, mapping[key], descriptor); + }); + } + + const { Debugger, Runtime } = inspector; + + let repl; // eslint-disable-line prefer-const + + // Things we want to keep around + const history = { control: [], debug: [] }; + const watchedExpressions = []; + const knownBreakpoints = []; + let pauseOnExceptionState = 'none'; + let lastCommand; + + // Things we need to reset when the app restarts + let knownScripts; + let currentBacktrace; + let selectedFrame; + let exitDebugRepl; + + function resetOnStart() { + knownScripts = {}; + currentBacktrace = null; + selectedFrame = null; + + if (exitDebugRepl) exitDebugRepl(); + exitDebugRepl = null; + } + resetOnStart(); + + const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY }; + function inspect(value) { + return util.inspect(value, INSPECT_OPTIONS); + } + + function print(value, oneline = false) { + const text = typeof value === 'string' ? value : inspect(value); + return inspector.print(text, oneline); + } + + function getCurrentLocation() { + if (!selectedFrame) { + throw new Error('Requires execution to be paused'); + } + return selectedFrame.location; + } + + function isCurrentScript(script) { + return selectedFrame && getCurrentLocation().scriptId === script.scriptId; + } + + function formatScripts(displayNatives = false) { + function isVisible(script) { + if (displayNatives) return true; + return !script.isNative || isCurrentScript(script); + } + + return Object.keys(knownScripts) + .map((scriptId) => knownScripts[scriptId]) + .filter(isVisible) + .map((script) => { + const isCurrent = isCurrentScript(script); + const { isNative, url } = script; + const name = `${getRelativePath(url)}${isNative ? ' ' : ''}`; + return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`; + }) + .join('\n'); + } + function listScripts(displayNatives = false) { + print(formatScripts(displayNatives)); + } + listScripts[util.inspect.custom] = function listWithoutInternal() { + return formatScripts(); + }; + + class ScopeSnapshot { + constructor(scope, properties) { + Object.assign(this, scope); + this.properties = new Map(properties.map((prop) => { + // console.error(prop); + const value = new RemoteObject(prop.value); + return [prop.name, value]; + })); + } + + [util.inspect.custom](depth, opts) { + const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`; + const name = this.name ? `<${this.name}>` : ''; + const prefix = `${type}${name} `; + return util.inspect(this.properties, opts) + .replace(/^Map /, prefix); + } + } + + class SourceSnippet { + constructor(location, delta, scriptSource) { + Object.assign(this, location); + this.scriptSource = scriptSource; + this.delta = delta; + } + + [util.inspect.custom](depth, options) { + const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this; + const start = Math.max(1, lineNumber - delta + 1); + const end = lineNumber + delta + 1; + + const lines = scriptSource.split('\n'); + return lines.slice(start - 1, end).map((lineText, offset) => { + const i = start + offset; + const isCurrent = i === (lineNumber + 1); + + const markedLine = isCurrent + ? markSourceColumn(lineText, columnNumber, options.colors) + : lineText; + + let isBreakpoint = false; + knownBreakpoints.forEach(({ location }) => { + if (!location) return; + if (scriptId === location.scriptId && + i === (location.lineNumber + 1)) { + isBreakpoint = true; + } + }); + + let prefixChar = ' '; + if (isCurrent) { + prefixChar = '>'; + } else if (isBreakpoint) { + prefixChar = '*'; + } + return `${leftPad(i, prefixChar, end)} ${markedLine}`; + }).join('\n'); + } + } + + function getSourceSnippet(location, delta = 5) { + const { scriptId } = location; + return Debugger.getScriptSource({ scriptId }) + .then(({ scriptSource }) => + new SourceSnippet(location, delta, scriptSource)); + } + + class CallFrame { + constructor(callFrame) { + Object.assign(this, callFrame); + } + + loadScopes() { + return Promise.all( + this.scopeChain + .filter((scope) => scope.type !== 'global') + .map((scope) => { + const { objectId } = scope.object; + return Runtime.getProperties({ + objectId, + generatePreview: true, + }).then(({ result }) => new ScopeSnapshot(scope, result)); + }) + ); + } + + list(delta = 5) { + return getSourceSnippet(this.location, delta); + } + } + + class Backtrace extends Array { + [util.inspect.custom]() { + return this.map((callFrame, idx) => { + const { + location: { scriptId, lineNumber, columnNumber }, + functionName + } = callFrame; + const name = functionName || '(anonymous)'; + + const script = knownScripts[scriptId]; + const relativeUrl = + (script && getRelativePath(script.url)) || ''; + const frameLocation = + `${relativeUrl}:${lineNumber + 1}:${columnNumber}`; + + return `#${idx} ${name} ${frameLocation}`; + }).join('\n'); + } + + static from(callFrames) { + return super.from(Array.from(callFrames).map((callFrame) => { + if (callFrame instanceof CallFrame) { + return callFrame; + } + return new CallFrame(callFrame); + })); + } + } + + function prepareControlCode(input) { + if (input === '\n') return lastCommand; + // exec process.title => exec("process.title"); + const match = input.match(/^\s*exec\s+([^\n]*)/); + if (match) { + lastCommand = `exec(${JSON.stringify(match[1])})`; + } else { + lastCommand = input; + } + return lastCommand; + } + + function evalInCurrentContext(code) { + // Repl asked for scope variables + if (code === '.scope') { + if (!selectedFrame) { + return Promise.reject(new Error('Requires execution to be paused')); + } + return selectedFrame.loadScopes(); + } + + if (selectedFrame) { + return Debugger.evaluateOnCallFrame({ + callFrameId: selectedFrame.callFrameId, + expression: code, + objectGroup: 'node-inspect', + generatePreview: true, + }).then(convertResultToRemoteObject); + } + return Runtime.evaluate({ + expression: code, + objectGroup: 'node-inspect', + generatePreview: true, + }).then(convertResultToRemoteObject); + } + + function controlEval(input, context, filename, callback) { + debuglog('eval:', input); + function returnToCallback(error, result) { + debuglog('end-eval:', input, error); + callback(error, result); + } + + try { + const code = prepareControlCode(input); + const result = vm.runInContext(code, context, filename); + + if (result && typeof result.then === 'function') { + toCallback(result, returnToCallback); + return; + } + returnToCallback(null, result); + } catch (e) { + returnToCallback(e); + } + } + + function debugEval(input, context, filename, callback) { + debuglog('eval:', input); + function returnToCallback(error, result) { + debuglog('end-eval:', input, error); + callback(error, result); + } + + try { + const result = evalInCurrentContext(input); + + if (result && typeof result.then === 'function') { + toCallback(result, returnToCallback); + return; + } + returnToCallback(null, result); + } catch (e) { + returnToCallback(e); + } + } + + function formatWatchers(verbose = false) { + if (!watchedExpressions.length) { + return Promise.resolve(''); + } + + const inspectValue = (expr) => + evalInCurrentContext(expr) + // .then(formatValue) + .catch((error) => `<${error.message}>`); + const lastIndex = watchedExpressions.length - 1; + + return Promise.all(watchedExpressions.map(inspectValue)) + .then((values) => { + const lines = watchedExpressions + .map((expr, idx) => { + const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`; + const value = inspect(values[idx], { colors: true }); + if (value.indexOf('\n') === -1) { + return `${prefix} ${value}`; + } + return `${prefix}\n ${value.split('\n').join('\n ')}`; + }); + return lines.join('\n'); + }) + .then((valueList) => { + return verbose ? `Watchers:\n${valueList}\n` : valueList; + }); + } + + function watchers(verbose = false) { + return formatWatchers(verbose).then(print); + } + + // List source code + function list(delta = 5) { + return selectedFrame.list(delta) + .then(null, (error) => { + print('You can\'t list source code right now'); + throw error; + }); + } + + function handleBreakpointResolved({ breakpointId, location }) { + const script = knownScripts[location.scriptId]; + const scriptUrl = script && script.url; + if (scriptUrl) { + Object.assign(location, { scriptUrl }); + } + const isExisting = knownBreakpoints.some((bp) => { + if (bp.breakpointId === breakpointId) { + Object.assign(bp, { location }); + return true; + } + return false; + }); + if (!isExisting) { + knownBreakpoints.push({ breakpointId, location }); + } + } + + function listBreakpoints() { + if (!knownBreakpoints.length) { + print('No breakpoints yet'); + return; + } + + function formatLocation(location) { + if (!location) return ''; + const script = knownScripts[location.scriptId]; + const scriptUrl = script ? script.url : location.scriptUrl; + return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`; + } + const breaklist = knownBreakpoints + .map((bp, idx) => `#${idx} ${formatLocation(bp.location)}`) + .join('\n'); + print(breaklist); + } + + function setBreakpoint(script, line, condition, silent) { + function registerBreakpoint({ breakpointId, actualLocation }) { + handleBreakpointResolved({ breakpointId, location: actualLocation }); + if (actualLocation && actualLocation.scriptId) { + if (!silent) return getSourceSnippet(actualLocation, 5); + } else { + print(`Warning: script '${script}' was not loaded yet.`); + } + return undefined; + } + + // setBreakpoint(): set breakpoint at current location + if (script === undefined) { + return Debugger + .setBreakpoint({ location: getCurrentLocation(), condition }) + .then(registerBreakpoint); + } + + // setBreakpoint(line): set breakpoint in current script at specific line + if (line === undefined && typeof script === 'number') { + const location = { + scriptId: getCurrentLocation().scriptId, + lineNumber: script - 1, + }; + return Debugger.setBreakpoint({ location, condition }) + .then(registerBreakpoint); + } + + if (typeof script !== 'string') { + throw new TypeError(`setBreakpoint() expects a string, got ${script}`); + } + + // setBreakpoint('fn()'): Break when a function is called + if (script.endsWith('()')) { + const debugExpr = `debug(${script.slice(0, -2)})`; + const debugCall = selectedFrame + ? Debugger.evaluateOnCallFrame({ + callFrameId: selectedFrame.callFrameId, + expression: debugExpr, + includeCommandLineAPI: true, + }) + : Runtime.evaluate({ + expression: debugExpr, + includeCommandLineAPI: true, + }); + return debugCall.then(({ result, wasThrown }) => { + if (wasThrown) return convertResultToError(result); + return undefined; // This breakpoint can't be removed the same way + }); + } + + // setBreakpoint('scriptname') + let scriptId = null; + let ambiguous = false; + if (knownScripts[script]) { + scriptId = script; + } else { + for (const id of Object.keys(knownScripts)) { + const scriptUrl = knownScripts[id].url; + if (scriptUrl && scriptUrl.indexOf(script) !== -1) { + if (scriptId !== null) { + ambiguous = true; + } + scriptId = id; + } + } + } + + if (ambiguous) { + print('Script name is ambiguous'); + return undefined; + } + if (line <= 0) { + print('Line should be a positive value'); + return undefined; + } + + if (scriptId !== null) { + const location = { scriptId, lineNumber: line - 1 }; + return Debugger.setBreakpoint({ location, condition }) + .then(registerBreakpoint); + } + + const escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1'); + const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`; + + return Debugger + .setBreakpointByUrl({ urlRegex, lineNumber: line - 1, condition }) + .then((bp) => { + // TODO: handle bp.locations in case the regex matches existing files + if (!bp.location) { // Fake it for now. + Object.assign(bp, { + actualLocation: { + scriptUrl: `.*/${script}$`, + lineNumber: line - 1, + }, + }); + } + return registerBreakpoint(bp); + }); + } + + function clearBreakpoint(url, line) { + const breakpoint = knownBreakpoints.find(({ location }) => { + if (!location) return false; + const script = knownScripts[location.scriptId]; + if (!script) return false; + return ( + script.url.indexOf(url) !== -1 && (location.lineNumber + 1) === line + ); + }); + if (!breakpoint) { + print(`Could not find breakpoint at ${url}:${line}`); + return Promise.resolve(); + } + return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }) + .then(() => { + const idx = knownBreakpoints.indexOf(breakpoint); + knownBreakpoints.splice(idx, 1); + }); + } + + function restoreBreakpoints() { + const lastBreakpoints = knownBreakpoints.slice(); + knownBreakpoints.length = 0; + const newBreakpoints = lastBreakpoints + .filter(({ location }) => !!location.scriptUrl) + .map(({ location }) => + setBreakpoint(location.scriptUrl, location.lineNumber + 1)); + if (!newBreakpoints.length) return; + Promise.all(newBreakpoints).then((results) => { + print(`${results.length} breakpoints restored.`); + }); + } + + function setPauseOnExceptions(state) { + return Debugger.setPauseOnExceptions({ state }) + .then(() => { + pauseOnExceptionState = state; + }); + } + + Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => { + // Save execution context's data + currentBacktrace = Backtrace.from(callFrames); + selectedFrame = currentBacktrace[0]; + const { scriptId, lineNumber } = selectedFrame.location; + + const breakType = reason === 'other' ? 'break' : reason; + const script = knownScripts[scriptId]; + const scriptUrl = script ? getRelativePath(script.url) : '[unknown]'; + print(`${breakType} in ${scriptUrl}:${lineNumber + 1}`); + + inspector.suspendReplWhile(() => + Promise.all([formatWatchers(true), selectedFrame.list(2)]) + .then(([watcherList, context]) => { + if (watcherList) { + return `${watcherList}\n${inspect(context)}`; + } + return context; + }).then(print)); + }); + + function handleResumed() { + currentBacktrace = null; + selectedFrame = null; + } + + Debugger.on('resumed', handleResumed); + + Debugger.on('breakpointResolved', handleBreakpointResolved); + + Debugger.on('scriptParsed', (script) => { + const { scriptId, url } = script; + if (url) { + knownScripts[scriptId] = Object.assign({ + isNative: isNativeUrl(url), + }, script); + } + }); + + function initializeContext(context) { + inspector.domainNames.forEach((domain) => { + Object.defineProperty(context, domain, { + value: inspector[domain], + enumerable: true, + configurable: true, + writeable: false, + }); + }); + + copyOwnProperties(context, { + get help() { + print(HELP); + }, + + get run() { + return inspector.run(); + }, + + get kill() { + return inspector.killChild(); + }, + + get restart() { + return inspector.run(); + }, + + get cont() { + handleResumed(); + return Debugger.resume(); + }, + + get next() { + handleResumed(); + return Debugger.stepOver(); + }, + + get step() { + handleResumed(); + return Debugger.stepInto(); + }, + + get out() { + handleResumed(); + return Debugger.stepOut(); + }, + + get pause() { + return Debugger.pause(); + }, + + get backtrace() { + return currentBacktrace; + }, + + get breakpoints() { + return listBreakpoints(); + }, + + exec(expr) { + return evalInCurrentContext(expr); + }, + + get watchers() { + return watchers(); + }, + + watch(expr) { + watchedExpressions.push(expr); + }, + + unwatch(expr) { + const index = watchedExpressions.indexOf(expr); + + // Unwatch by expression + // or + // Unwatch by watcher number + watchedExpressions.splice(index !== -1 ? index : +expr, 1); + }, + + get repl() { + // Don't display any default messages + const listeners = repl.rli.listeners('SIGINT').slice(0); + repl.rli.removeAllListeners('SIGINT'); + + const oldContext = repl.context; + + exitDebugRepl = () => { + // Restore all listeners + process.nextTick(() => { + listeners.forEach((listener) => { + repl.rli.on('SIGINT', listener); + }); + }); + + // Exit debug repl + repl.eval = controlEval; + + // Swap history + history.debug = repl.rli.history; + repl.rli.history = history.control; + + repl.context = oldContext; + repl.rli.setPrompt('debug> '); + repl.displayPrompt(); + + repl.rli.removeListener('SIGINT', exitDebugRepl); + repl.removeListener('exit', exitDebugRepl); + + exitDebugRepl = null; + }; + + // Exit debug repl on SIGINT + repl.rli.on('SIGINT', exitDebugRepl); + + // Exit debug repl on repl exit + repl.on('exit', exitDebugRepl); + + // Set new + repl.eval = debugEval; + repl.context = {}; + + // Swap history + history.control = repl.rli.history; + repl.rli.history = history.debug; + + repl.rli.setPrompt('> '); + + print('Press Ctrl + C to leave debug repl'); + repl.displayPrompt(); + }, + + get version() { + return Runtime.evaluate({ + expression: 'process.versions.v8', + contextId: 1, + returnByValue: true, + }).then(({ result }) => { + print(result.value); + }); + }, + + scripts: listScripts, + + setBreakpoint, + clearBreakpoint, + setPauseOnExceptions, + get breakOnException() { + return setPauseOnExceptions('all'); + }, + get breakOnUncaught() { + return setPauseOnExceptions('uncaught'); + }, + get breakOnNone() { + return setPauseOnExceptions('none'); + }, + + list, + }); + aliasProperties(context, SHORTCUTS); + } + + return function startRepl() { + const replOptions = { + prompt: 'debug> ', + input: inspector.stdin, + output: inspector.stdout, + eval: controlEval, + useGlobal: false, + ignoreUndefined: true, + }; + repl = Repl.start(replOptions); // eslint-disable-line prefer-const + initializeContext(repl.context); + repl.on('reset', initializeContext); + + repl.defineCommand('interrupt', () => { + // We want this for testing purposes where sending CTRL-C can be tricky. + repl.rli.emit('SIGINT'); + }); + + inspector.client.on('close', () => { + resetOnStart(); + }); + + inspector.client.on('ready', () => { + restoreBreakpoints(); + Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }); + }); + + return repl; + }; +} + +function runScript(script, scriptArgs, inspectPort, childPrint) { + return new Promise((resolve) => { + const args = [ + '--inspect', + `--debug-brk=${inspectPort}`, + ].concat([script], scriptArgs); + const child = spawn(process.execPath, args); + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', childPrint); + child.stderr.on('data', childPrint); + + let output = ''; + function waitForListenHint(text) { + output += text; + if (/chrome-devtools:\/\//.test(output)) { + child.stderr.removeListener('data', waitForListenHint); + resolve(child); + } + } + + child.stderr.on('data', waitForListenHint); + }); +} + +function createAgentProxy(domain, client) { + const agent = new EventEmitter(); + agent.then = (...args) => { + // TODO: potentially fetch the protocol and pretty-print it here. + const descriptor = { + [util.inspect.custom](depth, { stylize }) { + return stylize(`[Agent ${domain}]`, 'special'); + }, + }; + return Promise.resolve(descriptor).then(...args); + }; + + return new Proxy(agent, { + get(target, name) { + if (name in target) return target[name]; + return function callVirtualMethod(params) { + return client.callMethod(`${domain}.${name}`, params); + }; + }, + }); +} + +class NodeInspector { + constructor(options, stdin, stdout) { + this.options = options; + this.stdin = stdin; + this.stdout = stdout; + + this.paused = true; + this.child = null; + + if (options.script) { + this._runScript = runScript.bind(null, + options.script, + options.scriptArgs, + options.port, + this.childPrint.bind(this)); + } else { + this._runScript = () => Promise.resolve(null); + } + + this.client = new ProtocolClient(options.port, options.host); + + this.domainNames = ['Debugger', 'Runtime']; + this.domainNames.forEach((domain) => { + this[domain] = createAgentProxy(domain, this.client); + }); + this.handleDebugEvent = (fullName, params) => { + const [domain, name] = fullName.split('.'); + if (domain in this) { + this[domain].emit(name, params); + } + }; + this.client.on('debugEvent', this.handleDebugEvent); + const startRepl = createRepl(this); + + // Handle all possible exits + process.on('exit', () => this.killChild()); + process.once('SIGTERM', process.exit.bind(process, 0)); + process.once('SIGHUP', process.exit.bind(process, 0)); + + this.run() + .then(() => { + this.repl = startRepl(); + this.repl.on('exit', () => { + process.exit(0); + }); + this.paused = false; + }) + .then(null, (error) => process.nextTick(() => { throw error; })); + } + + suspendReplWhile(fn) { + this.repl.rli.pause(); + this.stdin.pause(); + this.paused = true; + return new Promise((resolve) => { + resolve(fn()); + }).then(() => { + this.paused = false; + this.repl.rli.resume(); + this.repl.displayPrompt(); + this.stdin.resume(); + }).then(null, (error) => process.nextTick(() => { throw error; })); + } + + killChild() { + this.client.reset(); + if (this.child) { + this.child.kill(); + this.child = null; + } + } + + run() { + this.killChild(); + return this._runScript().then((child) => { + this.child = child; + + let connectionAttempts = 0; + const attemptConnect = () => { + ++connectionAttempts; + debuglog('connection attempt #%d', connectionAttempts); + this.stdout.write('.'); + return this.client.connect() + .then(() => { + debuglog('connection established'); + }, (error) => { + debuglog('connect failed', error); + // If it's failed to connect 10 times then print failed message + if (connectionAttempts >= 10) { + this.stdout.write(' failed to connect, please retry\n'); + process.exit(1); + } + + return new Promise((resolve) => setTimeout(resolve, 500)) + .then(attemptConnect); + }); + }; + + const { host, port } = this.options; + this.print(`connecting to ${host}:${port} ..`, true); + return attemptConnect(); + }); + } + + clearLine() { + if (this.stdout.isTTY) { + this.stdout.cursorTo(0); + this.stdout.clearLine(1); + } else { + this.stdout.write('\b'); + } + } + + print(text, oneline = false) { + this.clearLine(); + this.stdout.write(oneline ? text : `${text}\n`); + } + + childPrint(text) { + this.print( + text.toString() + .split(/\r\n|\r|\n/g) + .filter((chunk) => !!chunk) + .map((chunk) => `< ${chunk}`) + .join('\n') + ); + if (!this.paused) { + this.repl.displayPrompt(true); + } + if (/Waiting for the debugger to disconnect\.\.\.\n$/.test(text)) { + this.killChild(); + } + } +} + +function parseArgv([target, ...args]) { + let host = '127.0.0.1'; + let port = exports.port; + let isRemote = false; + let script = target; + let scriptArgs = args; + + const hostMatch = target.match(/^([^:]+):(\d+)$/); + const portMatch = target.match(/^--port=(\d+)$/); + if (hostMatch) { + // Connecting to remote debugger + // `node-inspect localhost:9229` + host = hostMatch[1]; + port = parseInt(hostMatch[2], 10); + isRemote = true; + script = null; + } else if (portMatch) { + // Start debugger on custom port + // `node debug --port=8058 app.js` + port = parseInt(portMatch[1], 10); + script = args[0]; + scriptArgs = args.slice(1); + } + + return { + host, port, + isRemote, script, scriptArgs, + }; +} + +function startInspect(argv = process.argv.slice(2), + stdin = process.stdin, + stdout = process.stdout) { + /* eslint-disable no-console */ + if (argv.length < 1) { + console.error('Usage: node-inspect script.js'); + console.error(' node-inspect :'); + process.exit(1); + } + + const options = parseArgv(argv); + const inspector = new NodeInspector(options, stdin, stdout); + + stdin.resume(); + + function handleUnexpectedError(e) { + console.error('There was an internal error in node-inspect. ' + + 'Please report this bug.'); + console.error(e.message); + console.error(e.stack); + if (inspector.child) inspector.child.kill(); + process.exit(1); + } + + process.on('uncaughtException', handleUnexpectedError); + /* eslint-enable no-console */ +} +exports.start = startInspect; diff --git a/lib/cli.js b/lib/cli.js index 9cbe629..a4880df 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -21,4 +21,4 @@ */ 'use strict'; // ~= NativeModule.require('_debugger').start(); -require('./node-inspect').start(); +require('./_inspect').start(); diff --git a/lib/internal/inspect-protocol.js b/lib/internal/inspect-protocol.js deleted file mode 100644 index 75c6e0a..0000000 --- a/lib/internal/inspect-protocol.js +++ /dev/null @@ -1,354 +0,0 @@ -/* - * Copyright Node.js contributors. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -'use strict'; -const crypto = require('crypto'); -const { EventEmitter } = require('events'); -const http = require('http'); -const URL = require('url'); -const util = require('util'); - -const debuglog = util.debuglog('inspect'); - -const kOpCodeText = 0x1; -const kOpCodeClose = 0x8; - -const kFinalBit = 0x80; -const kReserved1Bit = 0x40; -const kReserved2Bit = 0x20; -const kReserved3Bit = 0x10; -const kOpCodeMask = 0xF; -const kMaskBit = 0x80; -const kPayloadLengthMask = 0x7F; - -const kMaxSingleBytePayloadLength = 125; -const kMaxTwoBytePayloadLength = 0xFFFF; -const kTwoBytePayloadLengthField = 126; -const kEightBytePayloadLengthField = 127; -const kMaskingKeyWidthInBytes = 4; - -function isEmpty(obj) { - return Object.keys(obj).length === 0; -} - -function unpackError({ code, message, data }) { - const err = new Error(`${message} - ${data}`); - err.code = code; - Error.captureStackTrace(err, unpackError); - return err; -} - -function encodeFrameHybi17(payload) { - const dataLength = payload.length; - - let singleByteLength; - let additionalLength; - if (dataLength > kMaxTwoBytePayloadLength) { - singleByteLength = kEightBytePayloadLengthField; - additionalLength = new Buffer(8); - let remaining = dataLength; - for (let i = 0; i < 8; ++i) { - additionalLength[7 - i] = remaining & 0xFF; - remaining >>= 8; - } - } else if (dataLength > kMaxSingleBytePayloadLength) { - singleByteLength = kTwoBytePayloadLengthField; - additionalLength = new Buffer(2); - additionalLength[0] = (dataLength & 0xFF00) >> 8; - additionalLength[1] = dataLength & 0xFF; - } else { - additionalLength = new Buffer(0); - singleByteLength = dataLength; - } - - const header = new Buffer([ - kFinalBit | kOpCodeText, - kMaskBit | singleByteLength, - ]); - - const mask = new Buffer(4); - const masked = new Buffer(dataLength); - for (let i = 0; i < dataLength; ++i) { - masked[i] = payload[i] ^ mask[i % kMaskingKeyWidthInBytes]; - } - - return Buffer.concat([header, additionalLength, mask, masked]); -} - -function decodeFrameHybi17(data) { - const dataAvailable = data.length; - const notComplete = { closed: false, payload: null, rest: data }; - let payloadOffset = 2; - if ((dataAvailable - payloadOffset) < 0) return notComplete; - - const firstByte = data[0]; - const secondByte = data[1]; - - const final = (firstByte & kFinalBit) !== 0; - const reserved1 = (firstByte & kReserved1Bit) !== 0; - const reserved2 = (firstByte & kReserved2Bit) !== 0; - const reserved3 = (firstByte & kReserved3Bit) !== 0; - const opCode = firstByte & kOpCodeMask; - const masked = (secondByte & kMaskBit) !== 0; - const compressed = reserved1; - if (compressed) { - throw new Error('Compressed frames not supported'); - } - if (!final || reserved2 || reserved3) { - throw new Error('Only compression extension is supported'); - } - - if (masked) { - throw new Error('Masked server frame - not supported'); - } - - let closed = false; - switch (opCode) { - case kOpCodeClose: - closed = true; - break; - case kOpCodeText: - break; - default: - throw new Error(`Unsupported op code ${opCode}`); - } - - let payloadLength = secondByte & kPayloadLengthMask; - switch (payloadLength) { - case kTwoBytePayloadLengthField: - payloadOffset += 2; - payloadLength = (data[2] << 8) + data[3]; - break; - - case kEightBytePayloadLengthField: - payloadOffset += 8; - payloadLength = 0; - for (let i = 0; i < 8; ++i) { - payloadLength <<= 8; - payloadLength |= data[2 + i]; - } - break; - - default: - // Nothing. We already have the right size. - } - if ((dataAvailable - payloadOffset - payloadLength) < 0) return notComplete; - - const payloadEnd = payloadOffset + payloadLength; - return { - payload: data.slice(payloadOffset, payloadEnd), - rest: data.slice(payloadEnd), - closed, - }; -} - -class Client extends EventEmitter { - constructor(port, host) { - super(); - this.handleChunk = this._handleChunk.bind(this); - - this._port = port; - this._host = host; - - this.reset(); - } - - _handleChunk(chunk) { - this._unprocessed = Buffer.concat([this._unprocessed, chunk]); - - while (this._unprocessed.length > 2) { - const { closed, payload: payloadBuffer, rest } = decodeFrameHybi17(this._unprocessed); - this._unprocessed = rest; - - if (closed) { - this.reset(); - return; - } - if (payloadBuffer === null) break; - - const payloadStr = payloadBuffer.toString(); - debuglog('< %s', payloadStr); - if (payloadStr[0] !== '{' || payloadStr[payloadStr.length - 1] !== '}') { - throw new Error(`Payload does not look like JSON: ${payloadStr}`); - } - let payload; - try { - payload = JSON.parse(payloadStr); - } catch (parseError) { - parseError.string = payloadStr; - throw parseError; - } - - const { id, method, params, result, error } = payload; - if (id) { - const handler = this._pending[id]; - if (handler) { - delete this._pending[id]; - handler(error, result); - } - } else if (method) { - this.emit('debugEvent', method, params); - this.emit(method, params); - } else { - throw new Error(`Unsupported response: ${payloadStr}`); - } - } - } - - reset() { - if (this._http) { - this._http.destroy(); - } - this._http = null; - this._lastId = 0; - this._socket = null; - this._pending = {}; - this._unprocessed = new Buffer(0); - } - - callMethod(method, params) { - return new Promise((resolve, reject) => { - if (!this._socket) { - reject(new Error('Use `run` to start the app again.')); - return; - } - const data = { id: ++this._lastId, method, params }; - this._pending[data.id] = (error, result) => { - if (error) reject(unpackError(error)); - else resolve(isEmpty(result) ? undefined : result); - }; - const json = JSON.stringify(data); - debuglog('> %s', json); - this._socket.write(encodeFrameHybi17(new Buffer(json))); - }); - } - - _fetchJSON(urlPath) { - return new Promise((resolve, reject) => { - const httpReq = http.get({ - host: this._host, - port: this._port, - path: urlPath, - }); - - const chunks = []; - - function onResponse(httpRes) { - function parseChunks() { - const resBody = Buffer.concat(chunks).toString(); - if (httpRes.statusCode !== 200) { - reject(new Error(`Unexpected ${httpRes.statusCode}: ${resBody}`)); - return; - } - try { - resolve(JSON.parse(resBody)); - } catch (parseError) { - reject(new Error(`Response did not contain valid JSON: ${resBody}`)); - return; - } - } - - httpRes.on('error', reject); - httpRes.on('data', chunk => chunks.push(chunk)); - httpRes.on('end', parseChunks); - } - - httpReq.on('error', reject); - httpReq.on('response', onResponse); - }); - } - - connect() { - return this._discoverWebsocketPath() - .then(urlPath => this._connectWebsocket(urlPath)); - } - - _discoverWebsocketPath() { - return this._fetchJSON('/json') - .then(([{ webSocketDebuggerUrl }]) => - URL.parse(webSocketDebuggerUrl).path); - } - - _connectWebsocket(urlPath) { - this.reset(); - - const key1 = crypto.randomBytes(16).toString('base64'); - debuglog('request websocket', key1); - - const httpReq = this._http = http.request({ - host: this._host, - port: this._port, - path: urlPath, - headers: { - Connection: 'Upgrade', - Upgrade: 'websocket', - 'Sec-WebSocket-Key': key1, - 'Sec-WebSocket-Version': '13', - }, - }); - httpReq.on('error', e => { - this.emit('error', e); - }); - httpReq.on('response', httpRes => { - if (httpRes.statusCode >= 400) { - process.stderr.write(`Unexpected HTTP response: ${httpRes.statusCode}\n`); - httpRes.pipe(process.stderr); - } else { - httpRes.pipe(process.stderr); - } - }); - - const handshakeListener = (res, socket) => { - // TODO: we *could* validate res.headers[sec-websocket-accept] - debuglog('websocket upgrade'); - - this._socket = socket; - socket.on('data', this.handleChunk); - socket.on('close', () => { - this.emit('close'); - }); - - Promise.all([ - this.callMethod('Runtime.enable'), - this.callMethod('Debugger.enable'), - this.callMethod('Debugger.setPauseOnExceptions', { state: 'none' }), - this.callMethod('Debugger.setAsyncCallStackDepth', { maxDepth: 0 }), - this.callMethod('Profiler.enable'), - this.callMethod('Profiler.setSamplingInterval', { interval: 100 }), - this.callMethod('Debugger.setBlackboxPatterns', { patterns: [] }), - this.callMethod('Runtime.runIfWaitingForDebugger'), - ]).then(() => { - this.emit('ready'); - }, error => { - this.emit('error', error); - }); - }; - - return new Promise((resolve, reject) => { - this.once('error', reject); - this.once('ready', resolve); - - httpReq.on('upgrade', handshakeListener); - httpReq.end(); - }); - } -} -module.exports = Client; diff --git a/lib/internal/inspect-repl.js b/lib/internal/inspect-repl.js deleted file mode 100644 index f76c95c..0000000 --- a/lib/internal/inspect-repl.js +++ /dev/null @@ -1,924 +0,0 @@ -/* - * Copyright Node.js contributors. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -'use strict'; -const Path = require('path'); -const Repl = require('repl'); -const util = require('util'); -const vm = require('vm'); - -const debuglog = util.debuglog('inspect'); - -const NATIVES = process.binding('natives'); - -const SHORTCUTS = { - cont: 'c', - next: 'n', - step: 's', - out: 'o', - backtrace: 'bt', - setBreakpoint: 'sb', - clearBreakpoint: 'cb', - run: 'r', -}; - -const HELP = ` -run, restart, r Run the application or reconnect -kill Kill a running application or disconnect - -cont, c Resume execution -next, n Continue to next line in current file -step, s Step into, potentially entering a function -out, o Step out, leaving the current function -backtrace, bt Print the current backtrace -list Print the source around the line where execution is currently paused - -setBreakpoint, sb Set a breakpoint -clearBreakpoint, cb Clear a breakpoint -breakpoints List all known breakpoints -breakOnException Pause execution whenever an exception is thrown -breakOnUncaught Pause execution whenever an exception isn't caught -breakOnNone Don't pause on exceptions (this is the default) - -watch(expr) Start watching the given expression -unwatch(expr) Stop watching an expression -watchers Print all watched expressions and their current values - -exec(expr) Evaluate the expression and print the value -repl Enter a debug repl that works like exec - -scripts List application scripts that are currently loaded -scripts(true) List all scripts (including node-internals) -`.trim(); - -function getRelativePath(filename) { - const dir = `${Path.resolve()}/`; - - // Change path to relative, if possible - if (filename.indexOf(dir) === 0) { - return filename.slice(dir.length); - } - return filename; -} - -function toCallback(promise, callback) { - function forward(...args) { - process.nextTick(() => callback(...args)); - } - promise.then(forward.bind(null, null), forward); -} - -// Adds spaces and prefix to number -// maxN is a maximum number we should have space for -function leftPad(n, prefix, maxN) { - const s = n.toString(); - const nchars = Math.max(2, String(maxN).length) + 1; - const nspaces = nchars - s.length - 1; - - return prefix + ' '.repeat(nspaces) + s; -} - -function markSourceColumn(sourceText, position, useColors) { - if (!sourceText) return ''; - - const head = sourceText.slice(0, position); - let tail = sourceText.slice(position); - - // Colourize char if stdout supports colours - if (useColors) { - tail = tail.replace(/(.+?)([^\w]|$)/, '\u001b[32m$1\u001b[39m$2'); - } - - // Return source line with coloured char at `position` - return [head, tail].join(''); -} - -function extractErrorMessage(stack) { - if (!stack) return ''; - const m = stack.match(/^\w+: ([^\n]+)/); - return m ? m[1] : stack; -} - -function convertResultToError(result) { - const { className, description } = result; - const err = new Error(extractErrorMessage(description)); - err.stack = description; - Object.defineProperty(err, 'name', { value: className }); - return err; -} - -class RemoteObject { - constructor(attributes) { - Object.assign(this, attributes); - if (this.type === 'number') { - this.value = this.unserializableValue ? +this.unserializableValue : +this.value; - } - } - - [util.inspect.custom](depth, opts) { - function formatProperty(prop) { - switch (prop.type) { - case 'string': - case 'undefined': - return util.inspect(prop.value, opts); - - case 'number': - case 'boolean': - return opts.stylize(prop.value, prop.type); - - case 'object': - case 'symbol': - if (prop.subtype === 'date') { - return util.inspect(new Date(prop.value), opts); - } - if (prop.subtype === 'array') { - return opts.stylize(prop.value, 'special'); - } - return opts.stylize(prop.value, prop.subtype || 'special'); - - default: - return prop.value; - } - } - switch (this.type) { - case 'boolean': - case 'number': - case 'string': - case 'undefined': - return util.inspect(this.value, opts); - - case 'symbol': - return opts.stylize(this.description, 'special'); - - case 'function': { - const fnNameMatch = (this.description).match(/^(?:function\*? )?([^(\s]+)\(/); - const fnName = fnNameMatch ? `: ${fnNameMatch[1]}` : ''; - const formatted = `[${this.className}${fnName}]`; - return opts.stylize(formatted, 'special'); - } - - case 'object': - switch (this.subtype) { - case 'date': - return util.inspect(new Date(this.description), opts); - - case 'null': - return util.inspect(null, opts); - - case 'regexp': - return opts.stylize(this.description, 'regexp'); - - default: - break; - } - if (this.preview) { - const props = this.preview.properties - .map((prop, idx) => { - const value = formatProperty(prop); - if (prop.name === `${idx}`) return value; - return `${prop.name}: ${value}`; - }); - if (this.preview.overflow) { - props.push('...'); - } - const singleLine = props.join(', '); - const propString = singleLine.length > 60 ? props.join(',\n ') : singleLine; - return this.subtype === 'array' ? `[ ${propString} ]` : `{ ${propString} }`; - } - return this.description; - - default: - return this.description; - } - } -} - -function convertResultToRemoteObject({ result, wasThrown }) { - if (wasThrown) return convertResultToError(result); - return new RemoteObject(result); -} - -function copyOwnProperties(target, source) { - Object.getOwnPropertyNames(source).forEach((prop) => { - Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop)); - }); -} - -function aliasProperties(target, mapping) { - Object.keys(mapping).forEach((key) => { - Object.defineProperty(target, mapping[key], Object.getOwnPropertyDescriptor(target, key)); - }); -} - -function createRepl(inspector) { - const { Debugger, Runtime } = inspector; - - let repl; // eslint-disable-line prefer-const - - // Things we want to keep around - const history = { control: [], debug: [] }; - const watchedExpressions = []; - const knownBreakpoints = []; - let pauseOnExceptionState = 'none'; - let lastCommand; - - // Things we need to reset when the app restarts - let knownScripts; - let currentBacktrace; - let selectedFrame; - let exitDebugRepl; - - function resetOnStart() { - knownScripts = {}; - currentBacktrace = null; - selectedFrame = null; - - if (exitDebugRepl) exitDebugRepl(); - exitDebugRepl = null; - } - resetOnStart(); - - const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY }; - function inspect(value) { - return util.inspect(value, INSPECT_OPTIONS); - } - - function print(value, oneline = false) { - const text = typeof value === 'string' ? value : inspect(value); - return inspector.print(text, oneline); - } - - function getCurrentLocation() { - if (!selectedFrame) { - throw new Error('Requires execution to be paused'); - } - return selectedFrame.location; - } - - function isCurrentScript(script) { - return selectedFrame && getCurrentLocation().scriptId === script.scriptId; - } - - function formatScripts(displayNatives = false) { - return Object.keys(knownScripts) - .map((scriptId) => knownScripts[scriptId]) - .filter((script) => displayNatives || !script.isNative || isCurrentScript(script)) - .map((script) => { - const isCurrent = isCurrentScript(script); - const { isNative, url } = script; - const name = `${getRelativePath(url)}${isNative ? ' ' : ''}`; - return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`; - }) - .join('\n'); - } - function listScripts(displayNatives = false) { - print(formatScripts(displayNatives)); - } - listScripts[util.inspect.custom] = function listWithoutInternal() { - return formatScripts(); - }; - - class ScopeSnapshot { - constructor(scope, properties) { - Object.assign(this, scope); - this.properties = new Map(properties.map((prop) => { - // console.error(prop); - const value = new RemoteObject(prop.value); - return [prop.name, value]; - })); - } - - [util.inspect.custom](depth, opts) { - const type = `${this.type[0].toUpperCase()}${this.type.slice(1)}`; - const name = this.name ? `<${this.name}>` : ''; - const prefix = `${type}${name} `; - return util.inspect(this.properties, opts) - .replace(/^Map /, prefix); - } - } - - class SourceSnippet { - constructor(location, delta, scriptSource) { - Object.assign(this, location); - this.scriptSource = scriptSource; - this.delta = delta; - } - - [util.inspect.custom](depth, options) { - const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this; - const start = Math.max(1, lineNumber - delta + 1); - const end = lineNumber + delta + 1; - - const lines = scriptSource.split('\n'); - return lines.slice(start - 1, end).map((lineText, offset) => { - const i = start + offset; - const isCurrent = i === (lineNumber + 1); - - const markedLine = isCurrent - ? markSourceColumn(lineText, columnNumber, options.colors) - : lineText; - - let isBreakpoint = false; - knownBreakpoints.forEach(({ location }) => { - if (location && location.scriptId === scriptId && (location.lineNumber + 1) === i) { - isBreakpoint = true; - } - }); - - let prefixChar = ' '; - if (isCurrent) { - prefixChar = '>'; - } else if (isBreakpoint) { - prefixChar = '*'; - } - return `${leftPad(i, prefixChar, end)} ${markedLine}`; - }).join('\n'); - } - } - - function getSourceSnippet(location, delta = 5) { - const { scriptId } = location; - return Debugger.getScriptSource({ scriptId }) - .then(({ scriptSource }) => new SourceSnippet(location, delta, scriptSource)); - } - - class CallFrame { - constructor(callFrame) { - Object.assign(this, callFrame); - } - - loadScopes() { - return Promise.all( - this.scopeChain - .filter((scope) => scope.type !== 'global') - .map((scope) => { - const { objectId } = scope.object; - return Runtime.getProperties({ - objectId, - generatePreview: true, - }).then(({ result }) => new ScopeSnapshot(scope, result)); - }) - ); - } - - list(delta = 5) { - return getSourceSnippet(this.location, delta); - } - } - - class Backtrace extends Array { - [util.inspect.custom]() { - return this.map((callFrame, idx) => { - const { location: { scriptId, lineNumber, columnNumber }, functionName } = callFrame; - const script = knownScripts[scriptId]; - const relativeUrl = (script && getRelativePath(script && script.url)) || ''; - const name = functionName || '(anonymous)'; - return `#${idx} ${name} ${relativeUrl}:${lineNumber + 1}:${columnNumber}`; - }).join('\n'); - } - - static from(callFrames) { - return super.from(Array.from(callFrames).map(callFrame => { - if (callFrame instanceof CallFrame) { - return callFrame; - } - return new CallFrame(callFrame); - })); - } - } - - function prepareControlCode(input) { - if (input === '\n') return lastCommand; - // exec process.title => exec("process.title"); - const match = input.match(/^\s*exec\s+([^\n]*)/); - if (match) { - lastCommand = `exec(${JSON.stringify(match[1])})`; - } else { - lastCommand = input; - } - return lastCommand; - } - - function evalInCurrentContext(code) { - // Repl asked for scope variables - if (code === '.scope') { - if (!selectedFrame) { - return Promise.reject(new Error('Requires execution to be paused')); - } - return selectedFrame.loadScopes(); - } - - if (selectedFrame) { - return Debugger.evaluateOnCallFrame({ - callFrameId: selectedFrame.callFrameId, - expression: code, - objectGroup: 'node-inspect', - generatePreview: true, - }).then(convertResultToRemoteObject); - } - return Runtime.evaluate({ - expression: code, - objectGroup: 'node-inspect', - generatePreview: true, - }).then(convertResultToRemoteObject); - } - - function controlEval(input, context, filename, callback) { - debuglog('eval:', input); - function returnToCallback(error, result) { - debuglog('end-eval:', input, error); - callback(error, result); - } - - try { - const code = prepareControlCode(input); - const result = vm.runInContext(code, context, filename); - - if (result && typeof result.then === 'function') { - toCallback(result, returnToCallback); - return; - } - returnToCallback(null, result); - } catch (e) { - returnToCallback(e); - } - } - - function debugEval(input, context, filename, callback) { - debuglog('eval:', input); - function returnToCallback(error, result) { - debuglog('end-eval:', input, error); - callback(error, result); - } - - try { - const result = evalInCurrentContext(input); - - if (result && typeof result.then === 'function') { - toCallback(result, returnToCallback); - return; - } - returnToCallback(null, result); - } catch (e) { - returnToCallback(e); - } - } - - function formatWatchers(verbose = false) { - if (!watchedExpressions.length) { - return Promise.resolve(''); - } - - const inspectValue = expr => - evalInCurrentContext(expr) - // .then(formatValue) - .catch(error => `<${error.message}>`); - - return Promise.all(watchedExpressions.map(inspectValue)) - .then(values => { - const lines = watchedExpressions - .map((expr, idx) => { - const prefix = `${leftPad(idx, ' ', watchedExpressions.length - 1)}: ${expr} =`; - const value = inspect(values[idx], { colors: true }); - if (value.indexOf('\n') === -1) { - return `${prefix} ${value}`; - } - return `${prefix}\n ${value.split('\n').join('\n ')}`; - }); - return lines.join('\n'); - }) - .then((valueList) => (verbose ? `Watchers:\n${valueList}\n` : valueList)); - } - - function watchers(verbose = false) { - return formatWatchers(verbose).then(print); - } - - // List source code - function list(delta = 5) { - return selectedFrame.list(delta) - .then(null, (error) => { - print('You can\'t list source code right now'); - throw error; - }); - } - - function handleBreakpointResolved({ breakpointId, location }) { - const script = knownScripts[location.scriptId]; - const scriptUrl = script && script.url; - if (scriptUrl) { - Object.assign(location, { scriptUrl }); - } - const isExisting = knownBreakpoints.some(bp => { - if (bp.breakpointId === breakpointId) { - Object.assign(bp, { location }); - return true; - } - return false; - }); - if (!isExisting) { - knownBreakpoints.push({ breakpointId, location }); - } - } - - function listBreakpoints() { - if (!knownBreakpoints.length) { - print('No breakpoints yet'); - return; - } - - function formatLocation(location) { - if (!location) return ''; - const script = knownScripts[location.scriptId]; - const scriptUrl = script ? script.url : location.scriptUrl; - return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`; - } - const breaklist = knownBreakpoints - .map((bp, idx) => `#${idx} ${formatLocation(bp.location)}`) - .join('\n'); - print(breaklist); - } - - function setBreakpoint(script, line, condition, silent) { - function registerBreakpoint({ breakpointId, actualLocation }) { - handleBreakpointResolved({ breakpointId, location: actualLocation }); - if (actualLocation && actualLocation.scriptId) { - if (!silent) return getSourceSnippet(actualLocation, 5); - } else { - print(`Warning: script '${script}' was not loaded yet.`); - } - return undefined; - } - - // setBreakpoint(): set breakpoint at current location - if (script === undefined) { - return Debugger.setBreakpoint({ location: getCurrentLocation(), condition }) - .then(registerBreakpoint); - } - - // setBreakpoint(line): set breakpoint in current script at specific line - if (line === undefined && typeof script === 'number') { - const location = { - scriptId: getCurrentLocation().scriptId, - lineNumber: script - 1, - }; - return Debugger.setBreakpoint({ location, condition }) - .then(registerBreakpoint); - } - - if (typeof script !== 'string') { - throw new TypeError(`setBreakpoint() expects a string, got ${script}`); - } - - // setBreakpoint('fn()'): Break when a function is called - if (script.endsWith('()')) { - const debugExpr = `debug(${script.slice(0, -2)})`; - const debugCall = selectedFrame - ? Debugger.evaluateOnCallFrame({ - callFrameId: selectedFrame.callFrameId, - expression: debugExpr, - includeCommandLineAPI: true, - }) - : Runtime.evaluate({ - expression: debugExpr, - includeCommandLineAPI: true, - }); - return debugCall.then(({ result, wasThrown }) => { - if (wasThrown) return convertResultToError(result); - return undefined; // This breakpoint can't be removed the same way - }); - } - - // setBreakpoint('scriptname') - let scriptId = null; - let ambiguous = false; - if (knownScripts[script]) { - scriptId = script; - } else { - for (const id of Object.keys(knownScripts)) { - if (knownScripts[id].url && knownScripts[id].url.indexOf(script) !== -1) { - if (scriptId !== null) { - ambiguous = true; - } - scriptId = id; - } - } - } - - if (ambiguous) { - print('Script name is ambiguous'); - return undefined; - } - if (line <= 0) { - print('Line should be a positive value'); - return undefined; - } - - if (scriptId !== null) { - const location = { scriptId, lineNumber: line - 1 }; - return Debugger.setBreakpoint({ location, condition }) - .then(registerBreakpoint); - } - - const escapedPath = script.replace(/([/\\.?*()^${}|[\]])/g, '\\$1'); - const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`; - - return Debugger.setBreakpointByUrl({ urlRegex, lineNumber: line - 1, condition }) - .then(bp => { - // TODO: handle bp.locations in case the regex matches existing files - if (!bp.location) { // Fake it for now. - Object.assign(bp, { - actualLocation: { scriptUrl: `.*/${script}$`, lineNumber: line - 1 }, - }); - } - return registerBreakpoint(bp); - }); - } - - function clearBreakpoint(url, line) { - const breakpoint = knownBreakpoints.find(({ location }) => { - if (!location) return false; - const script = knownScripts[location.scriptId]; - if (!script) return false; - return script.url.indexOf(url) !== -1 && (location.lineNumber + 1) === line; - }); - if (!breakpoint) { - print(`Could not find breakpoint at ${url}:${line}`); - return Promise.resolve(); - } - return Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }) - .then(() => { - const idx = knownBreakpoints.indexOf(breakpoint); - knownBreakpoints.splice(idx, 1); - }); - } - - function restoreBreakpoints() { - const lastBreakpoints = knownBreakpoints.slice(); - knownBreakpoints.length = 0; - const newBreakpoints = lastBreakpoints - .filter(({ location }) => !!location.scriptUrl) - .map(({ location }) => - setBreakpoint(location.scriptUrl, location.lineNumber + 1)); - if (!newBreakpoints.length) return; - Promise.all(newBreakpoints).then((results) => { - print(`${results.length} breakpoints restored.`); - }); - } - - function setPauseOnExceptions(state) { - return Debugger.setPauseOnExceptions({ state }) - .then(() => { - pauseOnExceptionState = state; - }); - } - - Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => { - // Save execution context's data - currentBacktrace = Backtrace.from(callFrames); - selectedFrame = currentBacktrace[0]; - const { scriptId, lineNumber } = selectedFrame.location; - - const script = knownScripts[scriptId]; - const scriptUrl = script ? getRelativePath(script.url) : '[unknown]'; - print(`${reason === 'other' ? 'break' : reason} in ${scriptUrl}:${lineNumber + 1}`); - - inspector.suspendReplWhile(() => - Promise.all([formatWatchers(true), selectedFrame.list(2)]) - .then(([watcherList, context]) => { - if (watcherList) { - return `${watcherList}\n${inspect(context)}`; - } - return context; - }).then(print)); - }); - - function handleResumed() { - currentBacktrace = null; - selectedFrame = null; - } - - Debugger.on('resumed', handleResumed); - - Debugger.on('breakpointResolved', handleBreakpointResolved); - - Debugger.on('scriptParsed', (script) => { - const { scriptId, url } = script; - if (url) { - knownScripts[scriptId] = Object.assign({ - isNative: url.replace('.js', '') in NATIVES || url === 'bootstrap_node.js', - }, script); - } - }); - - function initializeContext(context) { - inspector.domainNames.forEach(domain => { - Object.defineProperty(context, domain, { - value: inspector[domain], - enumerable: true, - configurable: true, - writeable: false, - }); - }); - - copyOwnProperties(context, { - get help() { - print(HELP); - }, - - get run() { - return inspector.run(); - }, - - get kill() { - return inspector.killChild(); - }, - - get restart() { - return inspector.run(); - }, - - get cont() { - handleResumed(); - return Debugger.resume(); - }, - - get next() { - handleResumed(); - return Debugger.stepOver(); - }, - - get step() { - handleResumed(); - return Debugger.stepInto(); - }, - - get out() { - handleResumed(); - return Debugger.stepOut(); - }, - - get pause() { - return Debugger.pause(); - }, - - get backtrace() { - return currentBacktrace; - }, - - get breakpoints() { - return listBreakpoints(); - }, - - exec(expr) { - return evalInCurrentContext(expr); - }, - - get watchers() { - return watchers(); - }, - - watch(expr) { - watchedExpressions.push(expr); - }, - - unwatch(expr) { - const index = watchedExpressions.indexOf(expr); - - // Unwatch by expression - // or - // Unwatch by watcher number - watchedExpressions.splice(index !== -1 ? index : +expr, 1); - }, - - get repl() { - // Don't display any default messages - const listeners = repl.rli.listeners('SIGINT').slice(0); - repl.rli.removeAllListeners('SIGINT'); - - const oldContext = repl.context; - - exitDebugRepl = () => { - // Restore all listeners - process.nextTick(() => { - listeners.forEach((listener) => { - repl.rli.on('SIGINT', listener); - }); - }); - - // Exit debug repl - repl.eval = controlEval; - - // Swap history - history.debug = repl.rli.history; - repl.rli.history = history.control; - - repl.context = oldContext; - repl.rli.setPrompt('debug> '); - repl.displayPrompt(); - - repl.rli.removeListener('SIGINT', exitDebugRepl); - repl.removeListener('exit', exitDebugRepl); - - exitDebugRepl = null; - }; - - // Exit debug repl on SIGINT - repl.rli.on('SIGINT', exitDebugRepl); - - // Exit debug repl on repl exit - repl.on('exit', exitDebugRepl); - - // Set new - repl.eval = debugEval; - repl.context = {}; - - // Swap history - history.control = repl.rli.history; - repl.rli.history = history.debug; - - repl.rli.setPrompt('> '); - - print('Press Ctrl + C to leave debug repl'); - repl.displayPrompt(); - }, - - get version() { - return Runtime.evaluate({ - expression: 'process.versions.v8', - contextId: 1, - returnByValue: true, - }).then(({ result }) => { - print(result.value); - }); - }, - - scripts: listScripts, - - setBreakpoint, - clearBreakpoint, - setPauseOnExceptions, - get breakOnException() { - return setPauseOnExceptions('all'); - }, - get breakOnUncaught() { - return setPauseOnExceptions('uncaught'); - }, - get breakOnNone() { - return setPauseOnExceptions('none'); - }, - - list, - }); - aliasProperties(context, SHORTCUTS); - } - - return function startRepl() { - const replOptions = { - prompt: 'debug> ', - input: inspector.stdin, - output: inspector.stdout, - eval: controlEval, - useGlobal: false, - ignoreUndefined: true, - }; - repl = Repl.start(replOptions); // eslint-disable-line prefer-const - initializeContext(repl.context); - repl.on('reset', initializeContext); - - repl.defineCommand('interrupt', () => { - // We want this for testing purposes where sending CTRL-C can be tricky. - repl.rli.emit('SIGINT'); - }); - - inspector.client.on('close', () => { - resetOnStart(); - }); - - inspector.client.on('ready', () => { - restoreBreakpoints(); - Debugger.setPauseOnExceptions({ state: pauseOnExceptionState }); - }); - - return repl; - }; -} -module.exports = createRepl; diff --git a/lib/node-inspect.js b/lib/node-inspect.js deleted file mode 100644 index d57faa1..0000000 --- a/lib/node-inspect.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright Node.js contributors. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ -'use strict'; -const { spawn } = require('child_process'); -const { EventEmitter } = require('events'); -const util = require('util'); - -const debuglog = util.debuglog('inspect'); - -const ProtocolClient = require('./internal/inspect-protocol'); -const createRepl = require('./internal/inspect-repl'); - -exports.port = 9229; - -function throwUnexpectedError(error) { - process.nextTick(() => { throw error; }); -} - -function delay(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function runScript(script, scriptArgs, inspectPort, childPrint) { - return new Promise((resolve) => { - const args = [ - '--inspect', - `--debug-brk=${inspectPort}`, - ].concat([script], scriptArgs); - const child = spawn(process.execPath, args); - child.stdout.setEncoding('utf8'); - child.stderr.setEncoding('utf8'); - child.stdout.on('data', childPrint); - child.stderr.on('data', childPrint); - - let output = ''; - function waitForListenHint(text) { - output += text; - if (/chrome-devtools:\/\//.test(output)) { - child.stderr.removeListener('data', waitForListenHint); - resolve(child); - } - } - - child.stderr.on('data', waitForListenHint); - }); -} - -function createAgentProxy(domain, client) { - const agent = new EventEmitter(); - agent.then = function retrieveDocs(...args) { - // TODO: potentially fetch the protocol and pretty-print it here. - const descriptor = { - [util.inspect.custom](depth, { stylize }) { - return stylize(`[Agent ${domain}]`, 'special'); - }, - }; - return Promise.resolve(descriptor).then(...args); - }; - - return new Proxy(agent, { - get(target, name) { - if (name in target) return target[name]; - return function callVirtualMethod(params) { - return client.callMethod(`${domain}.${name}`, params); - }; - }, - }); -} - -class NodeInspector { - constructor(options, stdin, stdout) { - this.options = options; - this.stdin = stdin; - this.stdout = stdout; - - this.paused = true; - this.child = null; - - this._runScript = options.script ? - runScript.bind(null, - options.script, options.scriptArgs, - options.port, - this.childPrint.bind(this) - ) : () => Promise.resolve(null); - - this.client = new ProtocolClient(options.port, options.host); - - this.domainNames = ['Debugger', 'Runtime']; - this.domainNames.forEach(domain => { - this[domain] = createAgentProxy(domain, this.client); - }); - this.handleDebugEvent = (fullName, params) => { - const [domain, name] = fullName.split('.'); - if (domain in this) { - this[domain].emit(name, params); - } - }; - this.client.on('debugEvent', this.handleDebugEvent); - const startRepl = createRepl(this); - - // Handle all possible exits - process.on('exit', () => this.killChild()); - process.once('SIGTERM', process.exit.bind(process, 0)); - process.once('SIGHUP', process.exit.bind(process, 0)); - - this.run() - .then(() => { - this.repl = startRepl(); - this.repl.on('exit', () => { - process.exit(0); - }); - this.paused = false; - }) - .then(null, throwUnexpectedError); - } - - suspendReplWhile(fn) { - this.repl.rli.pause(); - this.stdin.pause(); - this.paused = true; - return new Promise((resolve) => { - resolve(fn()); - }).then(() => { - this.paused = false; - this.repl.rli.resume(); - this.repl.displayPrompt(); - this.stdin.resume(); - }).then(null, throwUnexpectedError); - } - - killChild() { - this.client.reset(); - if (this.child) { - this.child.kill(); - this.child = null; - } - } - - run() { - this.killChild(); - return this._runScript().then((child) => { - this.child = child; - - let connectionAttempts = 0; - const attemptConnect = () => { - ++connectionAttempts; - debuglog('connection attempt #%d', connectionAttempts); - this.stdout.write('.'); - return this.client.connect() - .then(() => { - debuglog('connection established'); - }, (error) => { - debuglog('connect failed', error); - // If it's failed to connect 10 times then print failed message - if (connectionAttempts >= 10) { - this.stdout.write(' failed to connect, please retry\n'); - process.exit(1); - } - return delay(500).then(() => attemptConnect()); - }); - }; - - const { host, port } = this.options; - this.print(`connecting to ${host}:${port} ..`, true); - return attemptConnect(); - }); - } - - clearLine() { - if (this.stdout.isTTY) { - this.stdout.cursorTo(0); - this.stdout.clearLine(1); - } else { - this.stdout.write('\b'); - } - } - - print(text, oneline = false) { - this.clearLine(); - this.stdout.write(oneline ? text : `${text}\n`); - } - - childPrint(text) { - this.print( - text.toString() - .split(/\r\n|\r|\n/g) - .filter(chunk => !!chunk) - .map(chunk => `< ${chunk}`) - .join('\n') - ); - if (!this.paused) { - this.repl.displayPrompt(true); - } - if (/Waiting for the debugger to disconnect\.\.\.\n$/.test(text)) { - this.killChild(); - } - } -} - -function parseArgv([target, ...args]) { - let host = '127.0.0.1'; - let port = exports.port; - let isRemote = false; - let script = target; - let scriptArgs = args; - - const hostMatch = target.match(/^([^:]+):(\d+)$/); - const portMatch = target.match(/^--port=(\d+)$/); - if (hostMatch) { - // Connecting to remote debugger - // `node-inspect localhost:9229` - host = hostMatch[1]; - port = parseInt(hostMatch[2], 10); - isRemote = true; - script = null; - } else if (portMatch) { - // Start debugger on custom port - // `node debug --port=8058 app.js` - port = parseInt(portMatch[1], 10); - script = args[0]; - scriptArgs = args.slice(1); - } - - return { - host, port, - isRemote, script, scriptArgs, - }; -} - -function start(argv = process.argv.slice(2), - stdin = process.stdin, - stdout = process.stdout) { - /* eslint-disable no-console */ - if (argv.length < 1) { - console.error('Usage: node-inspect script.js'); - console.error(' node-inspect :'); - process.exit(1); - } - - const options = parseArgv(argv); - const inspector = new NodeInspector(options, stdin, stdout); - - stdin.resume(); - - function handleUnexpectedError(e) { - console.error('There was an internal error in node-inspect. ' + - 'Please report this bug.'); - console.error(e.message); - console.error(e.stack); - if (inspector.child) inspector.child.kill(); - process.exit(1); - } - - process.on('uncaughtException', handleUnexpectedError); - /* eslint-enable no-console */ -} -exports.start = start; diff --git a/package.json b/package.json index 88d5d42..006a328 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.9.1", "description": "Node Inspect", "license": "MIT", - "main": "lib/node-inspect.js", + "main": "lib/_inspect.js", "bin": "cli.js", "homepage": "https://github.com/buggerjs/node-inspect", "repository": { @@ -14,7 +14,7 @@ "url": "https://github.com/buggerjs/node-inspect/issues" }, "scripts": { - "pretest": "eslint lib test", + "pretest": "eslint --rulesdir=tools/eslint-rules lib test", "test": "tap 'test/**/*.test.js'", "posttest": "nlm verify" }, @@ -27,10 +27,7 @@ }, "dependencies": {}, "devDependencies": { - "eslint": "^2.0.0", - "eslint-config-groupon-node6": "^3.1.0", - "eslint-plugin-import": "^1.6.1", - "eslint-plugin-node": "^2.0.0", + "eslint": "^3.10.2", "nlm": "^2.0.0", "tap": "^7.1.2" }, diff --git a/test/.eslintrc b/test/.eslintrc deleted file mode 100644 index 419b38a..0000000 --- a/test/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "env": { - "mocha": true - }, - "rules": { - "func-names": 0 - } -} diff --git a/test/cli/break.test.js b/test/cli/break.test.js index 24b539a..1caa846 100644 --- a/test/cli/break.test.js +++ b/test/cli/break.test.js @@ -14,33 +14,45 @@ test('stepping through breakpoints', (t) => { return cli.waitFor(/break/) .then(() => cli.waitForPrompt()) .then(() => { - t.match(cli.output, 'break in examples/break.js:1', + t.match( + cli.output, + 'break in examples/break.js:1', 'pauses in the first line of the script'); - t.match(cli.output, - '> 1 (function (exports, require, module, __filename, __dirname) { const x = 10;', + t.match( + cli.output, + /> 1 \(function \([^)]+\) \{ const x = 10;/, 'shows the source and marks the current line'); }) .then(() => cli.stepCommand('n')) .then(() => { - t.match(cli.output, 'break in examples/break.js:2', + t.match( + cli.output, + 'break in examples/break.js:2', 'pauses in next line of the script'); - t.match(cli.output, + t.match( + cli.output, '> 2 let name = \'World\';', 'marks the 2nd line'); }) .then(() => cli.stepCommand('next')) .then(() => { - t.match(cli.output, 'break in examples/break.js:3', + t.match( + cli.output, + 'break in examples/break.js:3', 'pauses in next line of the script'); - t.match(cli.output, + t.match( + cli.output, '> 3 name = \'Robin\';', 'marks the 3nd line'); }) .then(() => cli.stepCommand('cont')) .then(() => { - t.match(cli.output, 'break in examples/break.js:10', + t.match( + cli.output, + 'break in examples/break.js:10', 'pauses on the next breakpoint'); - t.match(cli.output, + t.match( + cli.output, '>10 debugger;', 'marks the debugger line'); }) @@ -60,35 +72,47 @@ test('stepping through breakpoints', (t) => { .then(() => cli.command('list()')) .then(() => { t.match(cli.output, '>10 debugger;', 'prints and marks current line'); - t.strictDeepEqual(cli.parseSourceLines(), [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + t.strictDeepEqual( + cli.parseSourceLines(), + [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], 'prints 5 lines before and after'); }) .then(() => cli.command('list(2)')) .then(() => { t.match(cli.output, '>10 debugger;', 'prints and marks current line'); - t.strictDeepEqual(cli.parseSourceLines(), [8, 9, 10, 11, 12], + t.strictDeepEqual( + cli.parseSourceLines(), + [8, 9, 10, 11, 12], 'prints 2 lines before and after'); }) .then(() => cli.stepCommand('s')) .then(() => cli.stepCommand('')) .then(() => { - t.match(cli.output, 'break in timers.js', + t.match( + cli.output, + 'break in timers.js', 'entered timers.js'); }) .then(() => cli.stepCommand('cont')) .then(() => { - t.match(cli.output, 'break in examples/break.js:16', + t.match( + cli.output, + 'break in examples/break.js:16', 'found breakpoint we set above w/ line number only'); }) .then(() => cli.stepCommand('cont')) .then(() => { - t.match(cli.output, 'break in examples/break.js:6', + t.match( + cli.output, + 'break in examples/break.js:6', 'found breakpoint we set above w/ line number & script'); }) .then(() => cli.stepCommand('')) .then(() => { - t.match(cli.output, 'debugCommand in examples/break.js:14', + t.match( + cli.output, + 'debugCommand in examples/break.js:14', 'found function breakpoint we set above'); }) .then(() => cli.quit()) @@ -107,12 +131,16 @@ test('sb before loading file', (t) => { .then(() => cli.waitForPrompt()) .then(() => cli.command('sb("other.js", 3)')) .then(() => { - t.match(cli.output, 'not loaded yet', + t.match( + cli.output, + 'not loaded yet', 'warns that the script was not loaded yet'); }) .then(() => cli.stepCommand('cont')) .then(() => { - t.match(cli.output, 'break in examples/cjs/other.js:3', + t.match( + cli.output, + 'break in examples/cjs/other.js:3', 'found breakpoint in file that was not loaded yet'); }) .then(() => cli.quit()) @@ -151,7 +179,9 @@ test('clearBreakpoint', (t) => { }) .then(() => cli.stepCommand('cont')) .then(() => { - t.match(cli.output, 'break in examples/break.js:9', + t.match( + cli.output, + 'break in examples/break.js:9', 'hits the 2nd breakpoint because the 1st was cleared'); }) .then(() => cli.quit()) diff --git a/test/cli/exec.test.js b/test/cli/exec.test.js index c77da24..5c64713 100644 --- a/test/cli/exec.test.js +++ b/test/cli/exec.test.js @@ -19,7 +19,9 @@ test('examples/alive.js', (t) => { }) .then(() => cli.command('repl')) .then(() => { - t.match(cli.output, 'Press Ctrl + C to leave debug repl\n> ', + t.match( + cli.output, + 'Press Ctrl + C to leave debug repl\n> ', 'shows hint for how to leave repl'); t.notMatch(cli.output, 'debug>', 'changes the repl style'); }) @@ -27,7 +29,9 @@ test('examples/alive.js', (t) => { .then(() => cli.waitFor(/function/)) .then(() => cli.waitForPrompt()) .then(() => { - t.match(cli.output, '[ \'function\', \'function\' ]', 'can evaluate in the repl'); + t.match( + cli.output, + '[ \'function\', \'function\' ]', 'can evaluate in the repl'); t.match(cli.output, /> $/); }) .then(() => cli.ctrlC()) @@ -39,7 +43,9 @@ test('examples/alive.js', (t) => { .then(() => cli.command('cont')) .then(() => cli.command('exec [typeof heartbeat, typeof process.exit]')) .then(() => { - t.match(cli.output, '[ \'undefined\', \'function\' ]', + t.match( + cli.output, + '[ \'undefined\', \'function\' ]', 'non-paused exec can see global but not module-scope values'); }) .then(() => cli.quit()) @@ -59,7 +65,9 @@ test('exec .scope', (t) => { .then(() => cli.stepCommand('c')) .then(() => cli.command('exec .scope')) .then(() => { - t.match(cli.output, '\'moduleScoped\'', 'displays closure from module body'); + t.match( + cli.output, + '\'moduleScoped\'', 'displays closure from module body'); t.match(cli.output, '\'a\'', 'displays local / function arg'); t.match(cli.output, '\'l1\'', 'displays local scope'); t.notMatch(cli.output, '\'encodeURIComponent\'', 'omits global scope'); diff --git a/test/cli/invalid-args.test.js b/test/cli/invalid-args.test.js index be9bab2..c831d79 100644 --- a/test/cli/invalid-args.test.js +++ b/test/cli/invalid-args.test.js @@ -16,7 +16,9 @@ test('launch w/ invalid host:port', (t) => { const cli = startCLI(['localhost:914']); return cli.quit() .then((code) => { - t.match(cli.output, 'failed to connect', + t.match( + cli.output, + 'failed to connect', 'Tells the user that the connection failed'); t.equal(code, 1, 'exits with non-zero exit code'); }); diff --git a/test/cli/launch.test.js b/test/cli/launch.test.js index 32e1aa5..9062164 100644 --- a/test/cli/launch.test.js +++ b/test/cli/launch.test.js @@ -8,7 +8,10 @@ test('examples/empty.js', (t) => { return cli.waitForPrompt() .then(() => { t.match(cli.output, 'debug>', 'prints a prompt'); - t.match(cli.output, '< Debugger listening on port 9229', 'forwards child output'); + t.match( + cli.output, + '< Debugger listening on port 9229', + 'forwards child output'); }) .then(() => cli.command('["hello", "world"].join(" ")')) .then(() => { @@ -40,13 +43,17 @@ test('run after quit / restart', (t) => { .then(() => cli.waitForPrompt()) .then(() => cli.stepCommand('n')) .then(() => { - t.match(cli.output, 'break in examples/three-lines.js:2', + t.match( + cli.output, + 'break in examples/three-lines.js:2', 'steps to the 2nd line'); }) .then(() => cli.command('cont')) .then(() => cli.waitFor(/disconnect/)) .then(() => { - t.match(cli.output, 'Waiting for the debugger to disconnect', + t.match( + cli.output, + 'Waiting for the debugger to disconnect', 'the child was done'); }) .then(() => cli.command('cont')) @@ -57,17 +64,23 @@ test('run after quit / restart', (t) => { .then(() => cli.stepCommand('run')) .then(() => cli.waitForPrompt()) .then(() => { - t.match(cli.output, 'break in examples/three-lines.js:1', + t.match( + cli.output, + 'break in examples/three-lines.js:1', 'is back at the beginning'); }) .then(() => cli.stepCommand('n')) .then(() => { - t.match(cli.output, 'break in examples/three-lines.js:2', + t.match( + cli.output, + 'break in examples/three-lines.js:2', 'steps to the 2nd line'); }) .then(() => cli.stepCommand('restart')) .then(() => { - t.match(cli.output, 'break in examples/three-lines.js:1', + t.match( + cli.output, + 'break in examples/three-lines.js:1', 'is back at the beginning'); }) .then(() => cli.command('kill')) @@ -79,7 +92,9 @@ test('run after quit / restart', (t) => { .then(() => cli.stepCommand('run')) .then(() => cli.waitForPrompt()) .then(() => { - t.match(cli.output, 'break in examples/three-lines.js:1', + t.match( + cli.output, + 'break in examples/three-lines.js:1', 'is back at the beginning'); }) .then(() => cli.quit()) diff --git a/test/cli/low-level.test.js b/test/cli/low-level.test.js index 49a6254..326e5eb 100644 --- a/test/cli/low-level.test.js +++ b/test/cli/low-level.test.js @@ -16,11 +16,14 @@ test('Debugger agent direct access', (t) => { .then(() => cli.command('scripts')) .then(() => { const [, scriptId] = cli.output.match(/^\* (\d+): examples\/empty.js/); - return cli.command(`Debugger.getScriptSource({ scriptId: '${scriptId}' })`); + return cli.command( + `Debugger.getScriptSource({ scriptId: '${scriptId}' })` + ); }) .then(() => { - t.match(cli.output, - 'scriptSource: \'(function (exports, require, module, __filename, __dirname) { \\n});\''); + t.match( + cli.output, + /scriptSource: '\(function \([^)]+\) \{ \\n}\);'/); }) .then(() => cli.quit()) .then(null, onFatal); diff --git a/test/cli/scripts.test.js b/test/cli/scripts.test.js index ed8efd8..321c735 100644 --- a/test/cli/scripts.test.js +++ b/test/cli/scripts.test.js @@ -15,16 +15,24 @@ test('list scripts', (t) => { .then(() => cli.waitForPrompt()) .then(() => cli.command('scripts')) .then(() => { - t.match(cli.output, /^\* \d+: examples\/empty\.js/, + t.match( + cli.output, + /^\* \d+: examples\/empty\.js/, 'lists the user script'); - t.notMatch(cli.output, /\d+: module\.js /, + t.notMatch( + cli.output, + /\d+: module\.js /, 'omits node-internal scripts'); }) .then(() => cli.command('scripts(true)')) .then(() => { - t.match(cli.output, /\* \d+: examples\/empty\.js/, + t.match( + cli.output, + /\* \d+: examples\/empty\.js/, 'lists the user script'); - t.match(cli.output, /\d+: module\.js /, + t.match( + cli.output, + /\d+: module\.js /, 'includes node-internal scripts'); }) .then(() => cli.quit()) diff --git a/test/cli/start-cli.js b/test/cli/start-cli.js index 8fe192c..c3be6a7 100644 --- a/test/cli/start-cli.js +++ b/test/cli/start-cli.js @@ -63,8 +63,10 @@ function startCLI(args) { const timer = setTimeout(() => { tearDown(); // eslint-disable-line no-use-before-define - reject(new Error( - `Timeout (${timeout}) while waiting for ${pattern}; found: ${this.output}`)); + reject(new Error([ + `Timeout (${timeout}) while waiting for ${pattern}`, + `found: ${this.output}`, + ].join('; '))); }, timeout); function tearDown() { @@ -113,7 +115,10 @@ function startCLI(args) { this.flushOutput(); child.stdin.write(input); child.stdin.write('\n'); - return this.waitFor(/(?:assert|break|debugCommand|exception|other|promiseRejection) in/) + return this + .waitFor( + /(?:assert|break|debugCommand|exception|other|promiseRejection) in/ + ) .then(() => this.waitForPrompt()); }, diff --git a/test/cli/watchers.test.js b/test/cli/watchers.test.js index eb7769d..d66f008 100644 --- a/test/cli/watchers.test.js +++ b/test/cli/watchers.test.js @@ -32,7 +32,9 @@ test('stepping through breakpoints', (t) => { t.match(cli.output, '2: NaN = NaN'); t.match(cli.output, '3: true = true'); t.match(cli.output, '4: [1, 2] = [ 1, 2 ]'); - t.match(cli.output, /5: process\.env =\n\s+\{[\s\S]+,\n\s+\.\.\. \}/, + t.match( + cli.output, + /5: process\.env =\n\s+\{[\s\S]+,\n\s+\.\.\. \}/, 'shows "..." for process.env'); }) .then(() => cli.quit()) diff --git a/test/node-inspect.test.js b/test/node-inspect.test.js index 0f15a4e..12e7313 100644 --- a/test/node-inspect.test.js +++ b/test/node-inspect.test.js @@ -3,5 +3,7 @@ const tap = require('tap'); const nodeInspect = require('../'); -tap.equal(9229, nodeInspect.port, +tap.equal( + 9229, + nodeInspect.port, 'Uses the --inspect default port'); diff --git a/tools/eslint-rules/align-function-arguments.js b/tools/eslint-rules/align-function-arguments.js new file mode 100644 index 0000000..0155524 --- /dev/null +++ b/tools/eslint-rules/align-function-arguments.js @@ -0,0 +1,76 @@ +/** + * @fileoverview Align arguments in multiline function calls + * @author Rich Trott + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +function checkArgumentAlignment(context, node) { + + function isNodeFirstInLine(node, byEndLocation) { + const firstToken = byEndLocation === true ? context.getLastToken(node, 1) : + context.getTokenBefore(node); + const startLine = byEndLocation === true ? node.loc.end.line : + node.loc.start.line; + const endLine = firstToken ? firstToken.loc.end.line : -1; + + return startLine !== endLine; + } + + if (node.arguments.length === 0) + return; + + var msg = ''; + const first = node.arguments[0]; + var currentLine = first.loc.start.line; + const firstColumn = first.loc.start.column; + + const ignoreTypes = [ + 'ArrowFunctionExpression', + 'FunctionExpression', + 'ObjectExpression', + ]; + + const args = node.arguments; + + // For now, don't bother trying to validate potentially complicating things + // like closures. Different people will have very different ideas and it's + // probably best to implement configuration options. + if (args.some((node) => { return ignoreTypes.indexOf(node.type) !== -1; })) { + return; + } + + if (!isNodeFirstInLine(node)) { + return; + } + + var misaligned; + + args.slice(1).forEach((argument) => { + if (!misaligned) { + if (argument.loc.start.line === currentLine + 1) { + if (argument.loc.start.column !== firstColumn) { + if (isNodeFirstInLine(argument)) { + msg = 'Function argument in column ' + + `${argument.loc.start.column + 1}, ` + + `expected in ${firstColumn + 1}`; + misaligned = argument; + } + } + } + } + currentLine = argument.loc.start.line; + }); + + if (msg) + context.report(misaligned, msg); +} + +module.exports = function(context) { + return { + 'CallExpression': (node) => checkArgumentAlignment(context, node) + }; +}; diff --git a/tools/eslint-rules/align-multiline-assignment.js b/tools/eslint-rules/align-multiline-assignment.js new file mode 100644 index 0000000..80896b5 --- /dev/null +++ b/tools/eslint-rules/align-multiline-assignment.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Align multiline variable assignments + * @author Rich Trott + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +function getBinaryExpressionStarts(binaryExpression, starts) { + function getStartsFromOneSide(side, starts) { + starts.push(side.loc.start); + if (side.type === 'BinaryExpression') { + starts = getBinaryExpressionStarts(side, starts); + } + return starts; + } + + starts = getStartsFromOneSide(binaryExpression.left, starts); + starts = getStartsFromOneSide(binaryExpression.right, starts); + return starts; +} + +function checkExpressionAlignment(expression) { + if (!expression) + return; + + var msg = ''; + + switch (expression.type) { + case 'BinaryExpression': + var starts = getBinaryExpressionStarts(expression, []); + var startLine = starts[0].line; + const startColumn = starts[0].column; + starts.forEach((loc) => { + if (loc.line > startLine) { + startLine = loc.line; + if (loc.column !== startColumn) { + msg = 'Misaligned multiline assignment'; + } + } + }); + break; + } + return msg; +} + +function testAssignment(context, node) { + const msg = checkExpressionAlignment(node.right); + if (msg) + context.report(node, msg); +} + +function testDeclaration(context, node) { + node.declarations.forEach((declaration) => { + const msg = checkExpressionAlignment(declaration.init); + // const start = declaration.init.loc.start; + if (msg) + context.report(node, msg); + }); +} + +module.exports = function(context) { + return { + 'AssignmentExpression': (node) => testAssignment(context, node), + 'VariableDeclaration': (node) => testDeclaration(context, node) + }; +}; diff --git a/tools/eslint-rules/assert-fail-single-argument.js b/tools/eslint-rules/assert-fail-single-argument.js new file mode 100644 index 0000000..4ce7902 --- /dev/null +++ b/tools/eslint-rules/assert-fail-single-argument.js @@ -0,0 +1,30 @@ +/** + * @fileoverview Prohibit use of a single argument only in `assert.fail()`. It + * is almost always an error. + * @author Rich Trott + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +const msg = 'assert.fail() message should be third argument'; + +function isAssert(node) { + return node.callee.object && node.callee.object.name === 'assert'; +} + +function isFail(node) { + return node.callee.property && node.callee.property.name === 'fail'; +} + +module.exports = function(context) { + return { + 'CallExpression': function(node) { + if (isAssert(node) && isFail(node) && node.arguments.length === 1) { + context.report(node, msg); + } + } + }; +}; diff --git a/tools/eslint-rules/buffer-constructor.js b/tools/eslint-rules/buffer-constructor.js new file mode 100644 index 0000000..938598e --- /dev/null +++ b/tools/eslint-rules/buffer-constructor.js @@ -0,0 +1,25 @@ +/** + * @fileoverview Require use of new Buffer constructor methods in lib + * @author James M Snell + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ +const msg = 'Use of the Buffer() constructor has been deprecated. ' + + 'Please use either Buffer.alloc(), Buffer.allocUnsafe(), ' + + 'or Buffer.from()'; + +function test(context, node) { + if (node.callee.name === 'Buffer') { + context.report(node, msg); + } +} + +module.exports = function(context) { + return { + 'NewExpression': (node) => test(context, node), + 'CallExpression': (node) => test(context, node) + }; +}; diff --git a/tools/eslint-rules/new-with-error.js b/tools/eslint-rules/new-with-error.js new file mode 100644 index 0000000..655f34b --- /dev/null +++ b/tools/eslint-rules/new-with-error.js @@ -0,0 +1,31 @@ +/** + * @fileoverview Require `throw new Error()` rather than `throw Error()` + * @author Rich Trott + */ +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + + var errorList = context.options.length !== 0 ? context.options : ['Error']; + + return { + 'ThrowStatement': function(node) { + if (node.argument.type === 'CallExpression' && + errorList.indexOf(node.argument.callee.name) !== -1) { + context.report(node, 'Use new keyword when throwing.'); + } + } + }; +}; + +module.exports.schema = { + 'type': 'array', + 'additionalItems': { + 'type': 'string' + }, + 'uniqueItems': true +}; diff --git a/tools/eslint-rules/no-let-in-for-declaration.js b/tools/eslint-rules/no-let-in-for-declaration.js new file mode 100644 index 0000000..8b1a678 --- /dev/null +++ b/tools/eslint-rules/no-let-in-for-declaration.js @@ -0,0 +1,46 @@ +/** + * @fileoverview Prohibit the use of `let` as the loop variable + * in the initialization of for, and the left-hand + * iterator in forIn and forOf loops. + * + * @author Jessica Quynh Tran + */ + +'use strict'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + create(context) { + + const msg = 'Use of `let` as the loop variable in a for-loop is ' + + 'not recommended. Please use `var` instead.'; + + /** + * Report function to test if the for-loop is declared using `let`. + */ + function testForLoop(node) { + if (node.init && node.init.kind === 'let') { + context.report(node.init, msg); + } + } + + /** + * Report function to test if the for-in or for-of loop + * is declared using `let`. + */ + function testForInOfLoop(node) { + if (node.left && node.left.kind === 'let') { + context.report(node.left, msg); + } + } + + return { + 'ForStatement': testForLoop, + 'ForInStatement': testForInOfLoop, + 'ForOfStatement': testForInOfLoop + }; + } +}; diff --git a/tools/eslint-rules/prefer-assert-methods.js b/tools/eslint-rules/prefer-assert-methods.js new file mode 100644 index 0000000..fa345eb --- /dev/null +++ b/tools/eslint-rules/prefer-assert-methods.js @@ -0,0 +1,39 @@ +'use strict'; + +function isAssert(node) { + return node.expression && + node.expression.type === 'CallExpression' && + node.expression.callee && + node.expression.callee.name === 'assert'; +} + +function getFirstArg(expression) { + return expression.arguments && expression.arguments[0]; +} + +function parseError(method, op) { + return `'assert.${method}' should be used instead of '${op}'`; +} + +const preferedAssertMethod = { + '===': 'strictEqual', + '!==': 'notStrictEqual', + '==': 'equal', + '!=': 'notEqual' +}; + +module.exports = function(context) { + return { + ExpressionStatement(node) { + if (isAssert(node)) { + const arg = getFirstArg(node.expression); + if (arg && arg.type === 'BinaryExpression') { + const assertMethod = preferedAssertMethod[arg.operator]; + if (assertMethod) { + context.report(node, parseError(assertMethod, arg.operator)); + } + } + } + } + }; +}; diff --git a/tools/eslint-rules/require-buffer.js b/tools/eslint-rules/require-buffer.js new file mode 100644 index 0000000..c9818cb --- /dev/null +++ b/tools/eslint-rules/require-buffer.js @@ -0,0 +1,19 @@ +'use strict'; + +module.exports = function(context) { + function flagIt(reference) { + const msg = 'Use const Buffer = require(\'buffer\').Buffer; ' + + 'at the beginning of this file'; + context.report(reference.identifier, msg); + } + + return { + 'Program:exit': function() { + const globalScope = context.getScope(); + const variable = globalScope.set.get('Buffer'); + if (variable) { + variable.references.forEach(flagIt); + } + } + }; +}; diff --git a/tools/eslint-rules/required-modules.js b/tools/eslint-rules/required-modules.js new file mode 100644 index 0000000..3e4a8e8 --- /dev/null +++ b/tools/eslint-rules/required-modules.js @@ -0,0 +1,99 @@ +/** + * @fileoverview Require usage of specified node modules. + * @author Rich Trott + */ +'use strict'; + +var path = require('path'); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = function(context) { + // trim required module names + var requiredModules = context.options; + + var foundModules = []; + + // if no modules are required we don't need to check the CallExpressions + if (requiredModules.length === 0) { + return {}; + } + + /** + * Function to check if a node is a string literal. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a string literal. + */ + function isString(node) { + return node && node.type === 'Literal' && typeof node.value === 'string'; + } + + /** + * Function to check if a node is a require call. + * @param {ASTNode} node The node to check. + * @returns {boolean} If the node is a require call. + */ + function isRequireCall(node) { + return node.callee.type === 'Identifier' && node.callee.name === 'require'; + } + + /** + * Function to check if a node has an argument that is a required module and + * return its name. + * @param {ASTNode} node The node to check + * @returns {undefined|String} required module name or undefined + */ + function getRequiredModuleName(node) { + var moduleName; + + // node has arguments and first argument is string + if (node.arguments.length && isString(node.arguments[0])) { + var argValue = path.basename(node.arguments[0].value.trim()); + + // check if value is in required modules array + if (requiredModules.indexOf(argValue) !== -1) { + moduleName = argValue; + } + } + + return moduleName; + } + + return { + 'CallExpression': function(node) { + if (isRequireCall(node)) { + var requiredModuleName = getRequiredModuleName(node); + + if (requiredModuleName) { + foundModules.push(requiredModuleName); + } + } + }, + 'Program:exit': function(node) { + if (foundModules.length < requiredModules.length) { + var missingModules = requiredModules.filter( + function(module) { + return foundModules.indexOf(module === -1); + } + ); + missingModules.forEach(function(moduleName) { + context.report( + node, + 'Mandatory module "{{moduleName}}" must be loaded.', + { moduleName: moduleName } + ); + }); + } + } + }; +}; + +module.exports.schema = { + 'type': 'array', + 'additionalItems': { + 'type': 'string' + }, + 'uniqueItems': true +};