diff --git a/lib/console.js b/lib/console.js index ebd098e9affdff..85a89ecc990b8b 100644 --- a/lib/console.js +++ b/lib/console.js @@ -21,551 +21,4 @@ 'use strict'; -const { trace } = internalBinding('trace_events'); -const { - isStackOverflowError, - codes: { - ERR_CONSOLE_WRITABLE_STREAM, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - }, -} = require('internal/errors'); -const { previewEntries } = internalBinding('util'); -const { Buffer: { isBuffer } } = require('buffer'); -const util = require('util'); -const { - isTypedArray, isSet, isMap, isSetIterator, isMapIterator, -} = util.types; -const kCounts = Symbol('counts'); - -const kTraceConsoleCategory = 'node,node.console'; -const kTraceCount = 'C'.charCodeAt(0); -const kTraceBegin = 'b'.charCodeAt(0); -const kTraceEnd = 'e'.charCodeAt(0); -const kTraceInstant = 'n'.charCodeAt(0); - -const { - keys: ObjectKeys, - values: ObjectValues, -} = Object; -const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - -const { - isArray: ArrayIsArray, - from: ArrayFrom, -} = Array; - -// Lazy loaded for startup performance. -let cliTable; - -// Track amount of indentation required via `console.group()`. -const kGroupIndent = Symbol('kGroupIndent'); -const kFormatForStderr = Symbol('kFormatForStderr'); -const kFormatForStdout = Symbol('kFormatForStdout'); -const kGetInspectOptions = Symbol('kGetInspectOptions'); -const kColorMode = Symbol('kColorMode'); -const kIsConsole = Symbol('kIsConsole'); -const kWriteToConsole = Symbol('kWriteToConsole'); -const kBindProperties = Symbol('kBindProperties'); -const kBindStreamsEager = Symbol('kBindStreamsEager'); -const kBindStreamsLazy = Symbol('kBindStreamsLazy'); -const kUseStdout = Symbol('kUseStdout'); -const kUseStderr = Symbol('kUseStderr'); - -// This constructor is not used to construct the global console. -// It's exported for backwards compatibility. -function Console(options /* or: stdout, stderr, ignoreErrors = true */) { - // We have to test new.target here to see if this function is called - // with new, because we need to define a custom instanceof to accommodate - // the global console. - if (!new.target) { - return new Console(...arguments); - } - - if (!options || typeof options.write === 'function') { - options = { - stdout: options, - stderr: arguments[1], - ignoreErrors: arguments[2] - }; - } - - const { - stdout, - stderr = stdout, - ignoreErrors = true, - colorMode = 'auto' - } = options; - - if (!stdout || typeof stdout.write !== 'function') { - throw new ERR_CONSOLE_WRITABLE_STREAM('stdout'); - } - if (!stderr || typeof stderr.write !== 'function') { - throw new ERR_CONSOLE_WRITABLE_STREAM('stderr'); - } - - if (typeof colorMode !== 'boolean' && colorMode !== 'auto') - throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode); - - // bind the prototype functions to this Console instance - var keys = Object.keys(Console.prototype); - for (var v = 0; v < keys.length; v++) { - var k = keys[v]; - // We have to bind the methods grabbed from the instance instead of from - // the prototype so that users extending the Console can override them - // from the prototype chain of the subclass. - this[k] = this[k].bind(this); - } - - this[kBindStreamsEager](stdout, stderr); - this[kBindProperties](ignoreErrors, colorMode); -} - -const consolePropAttributes = { - writable: true, - enumerable: false, - configurable: true -}; - -// Fixup global.console instanceof global.console.Console -Object.defineProperty(Console, Symbol.hasInstance, { - value(instance) { - return instance[kIsConsole]; - } -}); - -// Eager version for the Console constructor -Console.prototype[kBindStreamsEager] = function(stdout, stderr) { - Object.defineProperties(this, { - '_stdout': { ...consolePropAttributes, value: stdout }, - '_stderr': { ...consolePropAttributes, value: stderr } - }); -}; - -// Lazily load the stdout and stderr from an object so we don't -// create the stdio streams when they are not even accessed -Console.prototype[kBindStreamsLazy] = function(object) { - let stdout; - let stderr; - Object.defineProperties(this, { - '_stdout': { - enumerable: false, - configurable: true, - get() { - if (!stdout) stdout = object.stdout; - return stdout; - }, - set(value) { stdout = value; } - }, - '_stderr': { - enumerable: false, - configurable: true, - get() { - if (!stderr) { stderr = object.stderr; } - return stderr; - }, - set(value) { stderr = value; } - } - }); -}; - -Console.prototype[kBindProperties] = function(ignoreErrors, colorMode) { - Object.defineProperties(this, { - '_stdoutErrorHandler': { - ...consolePropAttributes, - value: createWriteErrorHandler(this, kUseStdout) - }, - '_stderrErrorHandler': { - ...consolePropAttributes, - value: createWriteErrorHandler(this, kUseStderr) - }, - '_ignoreErrors': { - ...consolePropAttributes, - value: Boolean(ignoreErrors) - }, - '_times': { ...consolePropAttributes, value: new Map() } - }); - - // TODO(joyeecheung): use consolePropAttributes for these - // Corresponds to https://console.spec.whatwg.org/#count-map - this[kCounts] = new Map(); - this[kColorMode] = colorMode; - this[kIsConsole] = true; - this[kGroupIndent] = ''; -}; - -// Make a function that can serve as the callback passed to `stream.write()`. -function createWriteErrorHandler(instance, streamSymbol) { - return (err) => { - // This conditional evaluates to true if and only if there was an error - // that was not already emitted (which happens when the _write callback - // is invoked asynchronously). - const stream = streamSymbol === kUseStdout ? - instance._stdout : instance._stderr; - if (err !== null && !stream._writableState.errorEmitted) { - // If there was an error, it will be emitted on `stream` as - // an `error` event. Adding a `once` listener will keep that error - // from becoming an uncaught exception, but since the handler is - // removed after the event, non-console.* writes won't be affected. - // we are only adding noop if there is no one else listening for 'error' - if (stream.listenerCount('error') === 0) { - stream.on('error', noop); - } - } - }; -} - -Console.prototype[kWriteToConsole] = function(streamSymbol, string) { - const ignoreErrors = this._ignoreErrors; - const groupIndent = this[kGroupIndent]; - - const useStdout = streamSymbol === kUseStdout; - const stream = useStdout ? this._stdout : this._stderr; - const errorHandler = useStdout ? - this._stdoutErrorHandler : this._stderrErrorHandler; - - if (groupIndent.length !== 0) { - if (string.indexOf('\n') !== -1) { - string = string.replace(/\n/g, `\n${groupIndent}`); - } - string = groupIndent + string; - } - string += '\n'; - - if (ignoreErrors === false) return stream.write(string); - - // There may be an error occurring synchronously (e.g. for files or TTYs - // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so - // handle both situations. - try { - // Add and later remove a noop error handler to catch synchronous errors. - stream.once('error', noop); - - stream.write(string, errorHandler); - } catch (e) { - // console is a debugging utility, so it swallowing errors is not desirable - // even in edge cases such as low stack space. - if (isStackOverflowError(e)) - throw e; - // Sorry, there's no proper way to pass along the error here. - } finally { - stream.removeListener('error', noop); - } -}; - -const kColorInspectOptions = { colors: true }; -const kNoColorInspectOptions = {}; -Console.prototype[kGetInspectOptions] = function(stream) { - let color = this[kColorMode]; - if (color === 'auto') { - color = stream.isTTY && ( - typeof stream.getColorDepth === 'function' ? - stream.getColorDepth() > 2 : true); - } - - return color ? kColorInspectOptions : kNoColorInspectOptions; -}; - -Console.prototype[kFormatForStdout] = function(args) { - const opts = this[kGetInspectOptions](this._stdout); - return util.formatWithOptions(opts, ...args); -}; - -Console.prototype[kFormatForStderr] = function(args) { - const opts = this[kGetInspectOptions](this._stderr); - return util.formatWithOptions(opts, ...args); -}; - -Console.prototype.log = function log(...args) { - this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args)); -}; - -Console.prototype.debug = Console.prototype.log; -Console.prototype.info = Console.prototype.log; -Console.prototype.dirxml = Console.prototype.log; - -Console.prototype.warn = function warn(...args) { - this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); -}; - -Console.prototype.error = Console.prototype.warn; - -Console.prototype.dir = function dir(object, options) { - options = Object.assign({ - customInspect: false - }, this[kGetInspectOptions](this._stdout), options); - this[kWriteToConsole](kUseStdout, util.inspect(object, options)); -}; - -Console.prototype.time = function time(label = 'default') { - // Coerces everything other than Symbol to a string - label = `${label}`; - if (this._times.has(label)) { - process.emitWarning(`Label '${label}' already exists for console.time()`); - return; - } - trace(kTraceBegin, kTraceConsoleCategory, `time::${label}`, 0); - this._times.set(label, process.hrtime()); -}; - -Console.prototype.timeEnd = function timeEnd(label = 'default') { - // Coerces everything other than Symbol to a string - label = `${label}`; - const hasWarned = timeLogImpl(this, 'timeEnd', label); - trace(kTraceEnd, kTraceConsoleCategory, `time::${label}`, 0); - if (!hasWarned) { - this._times.delete(label); - } -}; - -Console.prototype.timeLog = function timeLog(label = 'default', ...data) { - // Coerces everything other than Symbol to a string - label = `${label}`; - timeLogImpl(this, 'timeLog', label, data); - trace(kTraceInstant, kTraceConsoleCategory, `time::${label}`, 0); -}; - -// Returns true if label was not found -function timeLogImpl(self, name, label, data) { - const time = self._times.get(label); - if (!time) { - process.emitWarning(`No such label '${label}' for console.${name}()`); - return true; - } - const duration = process.hrtime(time); - const ms = duration[0] * 1000 + duration[1] / 1e6; - if (data === undefined) { - self.log('%s: %sms', label, ms.toFixed(3)); - } else { - self.log('%s: %sms', label, ms.toFixed(3), ...data); - } - return false; -} - -Console.prototype.trace = function trace(...args) { - const err = { - name: 'Trace', - message: this[kFormatForStderr](args) - }; - Error.captureStackTrace(err, trace); - this.error(err.stack); -}; - -Console.prototype.assert = function assert(expression, ...args) { - if (!expression) { - args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`; - this.warn(...args); // the arguments will be formatted in warn() again - } -}; - -// Defined by: https://console.spec.whatwg.org/#clear -Console.prototype.clear = function clear() { - // It only makes sense to clear if _stdout is a TTY. - // Otherwise, do nothing. - if (this._stdout.isTTY) { - // The require is here intentionally to avoid readline being - // required too early when console is first loaded. - const { cursorTo, clearScreenDown } = require('readline'); - cursorTo(this._stdout, 0, 0); - clearScreenDown(this._stdout); - } -}; - -// Defined by: https://console.spec.whatwg.org/#count -Console.prototype.count = function count(label = 'default') { - // Ensures that label is a string, and only things that can be - // coerced to strings. e.g. Symbol is not allowed - label = `${label}`; - const counts = this[kCounts]; - let count = counts.get(label); - if (count === undefined) - count = 1; - else - count++; - counts.set(label, count); - trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, count); - this.log(`${label}: ${count}`); -}; - -// Defined by: https://console.spec.whatwg.org/#countreset -Console.prototype.countReset = function countReset(label = 'default') { - const counts = this[kCounts]; - if (!counts.has(label)) { - process.emitWarning(`Count for '${label}' does not exist`); - return; - } - trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, 0); - counts.delete(`${label}`); -}; - -Console.prototype.group = function group(...data) { - if (data.length > 0) { - this.log(...data); - } - this[kGroupIndent] += ' '; -}; -Console.prototype.groupCollapsed = Console.prototype.group; - -Console.prototype.groupEnd = function groupEnd() { - this[kGroupIndent] = - this[kGroupIndent].slice(0, this[kGroupIndent].length - 2); -}; - -const keyKey = 'Key'; -const valuesKey = 'Values'; -const indexKey = '(index)'; -const iterKey = '(iteration index)'; - -const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v); - -// https://console.spec.whatwg.org/#table -Console.prototype.table = function(tabularData, properties) { - if (properties !== undefined && !ArrayIsArray(properties)) - throw new ERR_INVALID_ARG_TYPE('properties', 'Array', properties); - - if (tabularData === null || typeof tabularData !== 'object') - return this.log(tabularData); - - if (cliTable === undefined) cliTable = require('internal/cli_table'); - const final = (k, v) => this.log(cliTable(k, v)); - - const inspect = (v) => { - const opt = { depth: 0, maxArrayLength: 3 }; - if (v !== null && typeof v === 'object' && - !isArray(v) && ObjectKeys(v).length > 2) - opt.depth = -1; - Object.assign(opt, this[kGetInspectOptions](this._stdout)); - return util.inspect(v, opt); - }; - const getIndexArray = (length) => ArrayFrom({ length }, (_, i) => inspect(i)); - - const mapIter = isMapIterator(tabularData); - let isKeyValue = false; - let i = 0; - if (mapIter) { - const res = previewEntries(tabularData, true); - tabularData = res[0]; - isKeyValue = res[1]; - } - - if (isKeyValue || isMap(tabularData)) { - const keys = []; - const values = []; - let length = 0; - if (mapIter) { - for (; i < tabularData.length / 2; ++i) { - keys.push(inspect(tabularData[i * 2])); - values.push(inspect(tabularData[i * 2 + 1])); - length++; - } - } else { - for (const [k, v] of tabularData) { - keys.push(inspect(k)); - values.push(inspect(v)); - length++; - } - } - return final([ - iterKey, keyKey, valuesKey - ], [ - getIndexArray(length), - keys, - values, - ]); - } - - const setIter = isSetIterator(tabularData); - if (setIter) - tabularData = previewEntries(tabularData); - - const setlike = setIter || (mapIter && !isKeyValue) || isSet(tabularData); - if (setlike) { - const values = []; - let length = 0; - for (const v of tabularData) { - values.push(inspect(v)); - length++; - } - return final([setlike ? iterKey : indexKey, valuesKey], [ - getIndexArray(length), - values, - ]); - } - - const map = {}; - let hasPrimitives = false; - const valuesKeyArray = []; - const indexKeyArray = ObjectKeys(tabularData); - - for (; i < indexKeyArray.length; i++) { - const item = tabularData[indexKeyArray[i]]; - const primitive = item === null || - (typeof item !== 'function' && typeof item !== 'object'); - if (properties === undefined && primitive) { - hasPrimitives = true; - valuesKeyArray[i] = inspect(item); - } else { - const keys = properties || ObjectKeys(item); - for (const key of keys) { - if (map[key] === undefined) - map[key] = []; - if ((primitive && properties) || !hasOwnProperty(item, key)) - map[key][i] = ''; - else - map[key][i] = item == null ? item : inspect(item[key]); - } - } - } - - const keys = ObjectKeys(map); - const values = ObjectValues(map); - if (hasPrimitives) { - keys.push(valuesKey); - values.push(valuesKeyArray); - } - keys.unshift(indexKey); - values.unshift(indexKeyArray); - - return final(keys, values); -}; - -function noop() {} - -// See https://console.spec.whatwg.org/#console-namespace -// > For historical web-compatibility reasons, the namespace object -// > for console must have as its [[Prototype]] an empty object, -// > created as if by ObjectCreate(%ObjectPrototype%), -// > instead of %ObjectPrototype%. - -// Since in Node.js, the Console constructor has been exposed through -// require('console'), we need to keep the Console constructor but -// we cannot actually use `new Console` to construct the global console. -// Therefore, the console.Console.prototype is not -// in the global console prototype chain anymore. - -// TODO(joyeecheung): -// - Move the Console constructor into internal/console.js -// - Move the global console creation code along with the inspector console -// wrapping code in internal/bootstrap/node.js into a separate file. -// - Make this file a simple re-export of those two files. -const globalConsole = Object.create({}); - -// Since Console is not on the prototype chain of the global console, -// the symbol properties on Console.prototype have to be looked up from -// the global console itself. In addition, we need to make the global -// console a namespace by binding the console methods directly onto -// the global console with the receiver fixed. -for (const prop of Reflect.ownKeys(Console.prototype)) { - if (prop === 'constructor') { continue; } - const desc = Reflect.getOwnPropertyDescriptor(Console.prototype, prop); - if (typeof desc.value === 'function') { // fix the receiver - desc.value = desc.value.bind(globalConsole); - } - Reflect.defineProperty(globalConsole, prop, desc); -} - -globalConsole[kBindStreamsLazy](process); -globalConsole[kBindProperties](true, 'auto'); - -module.exports = globalConsole; -module.exports.Console = Console; +module.exports = require('internal/console/global'); diff --git a/lib/inspector.js b/lib/inspector.js index 6988eccf82c9ef..14ea01e6adc6c7 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -12,7 +12,6 @@ const { const { validateString } = require('internal/validators'); const util = require('util'); const { Connection, open, url } = process.binding('inspector'); -const { originalConsole } = require('internal/process/per_thread'); if (!Connection) throw new ERR_INSPECTOR_NOT_AVAILABLE(); @@ -103,6 +102,8 @@ module.exports = { open: (port, host, wait) => open(port, host, !!wait), close: process._debugEnd, url: url, - console: originalConsole, + // This is dynamically added during bootstrap, + // where the console from the VM is still available + console: require('internal/console/inspector').consoleFromVM, Session }; diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 21544bceb463d1..b081e403082980 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -126,8 +126,6 @@ const browserGlobals = !process._noBrowserGlobals; if (browserGlobals) { - // we are setting this here to forward it to the inspector later - perThreadSetup.originalConsole = global.console; setupGlobalTimeouts(); setupGlobalConsole(); setupGlobalURL(); @@ -438,16 +436,25 @@ } function setupGlobalConsole() { - const originalConsole = global.console; - // Setup Node.js global.console. - const wrappedConsole = NativeModule.require('console'); + const consoleFromVM = global.console; + const consoleFromNode = + NativeModule.require('internal/console/global'); + // Override global console from the one provided by the VM + // to the one implemented by Node.js Object.defineProperty(global, 'console', { configurable: true, enumerable: false, - value: wrappedConsole, + value: consoleFromNode, writable: true }); - setupInspector(originalConsole, wrappedConsole); + // TODO(joyeecheung): can we skip this if inspector is not active? + if (process.config.variables.v8_enable_inspector) { + const inspector = + NativeModule.require('internal/console/inspector'); + inspector.addInspectorApis(consoleFromNode, consoleFromVM); + // This will be exposed by `require('inspector').console` later. + inspector.consoleFromVM = consoleFromVM; + } } function setupGlobalURL() { @@ -520,41 +527,6 @@ NativeModule.require('internal/domexception'); } - function setupInspector(originalConsole, wrappedConsole) { - if (!process.config.variables.v8_enable_inspector) { - return; - } - const CJSModule = NativeModule.require('internal/modules/cjs/loader'); - const { addCommandLineAPI, consoleCall } = process.binding('inspector'); - // Setup inspector command line API. - const { makeRequireFunction } = - NativeModule.require('internal/modules/cjs/helpers'); - const path = NativeModule.require('path'); - const cwd = tryGetCwd(path); - - const consoleAPIModule = new CJSModule(''); - consoleAPIModule.paths = - CJSModule._nodeModulePaths(cwd).concat(CJSModule.globalPaths); - addCommandLineAPI('require', makeRequireFunction(consoleAPIModule)); - const config = {}; - for (const key of Object.keys(wrappedConsole)) { - if (!originalConsole.hasOwnProperty(key)) - continue; - // If global console has the same method as inspector console, - // then wrap these two methods into one. Native wrapper will preserve - // the original stack. - wrappedConsole[key] = consoleCall.bind(wrappedConsole, - originalConsole[key], - wrappedConsole[key], - config); - } - for (const key of Object.keys(originalConsole)) { - if (wrappedConsole.hasOwnProperty(key)) - continue; - wrappedConsole[key] = originalConsole[key]; - } - } - function noop() {} function setupProcessFatal() { @@ -633,17 +605,6 @@ } } - function tryGetCwd(path) { - try { - return process.cwd(); - } catch { - // getcwd(3) can fail if the current working directory has been deleted. - // Fall back to the directory name of the (absolute) executable path. - // It's not really correct but what are the alternatives? - return path.dirname(process.execPath); - } - } - function wrapForBreakOnFirstLine(source) { if (!process._breakFirstLine) return source; @@ -654,6 +615,7 @@ function evalScript(name, body = wrapForBreakOnFirstLine(process._eval)) { const CJSModule = NativeModule.require('internal/modules/cjs/loader'); const path = NativeModule.require('path'); + const { tryGetCwd } = NativeModule.require('internal/util'); const cwd = tryGetCwd(path); const module = new CJSModule(name); diff --git a/lib/internal/console/constructor.js b/lib/internal/console/constructor.js new file mode 100644 index 00000000000000..f607bf4648bbff --- /dev/null +++ b/lib/internal/console/constructor.js @@ -0,0 +1,518 @@ +'use strict'; + +// The Console constructor is not actually used to construct the global +// console. It's exported for backwards compatibility. + +const { trace } = internalBinding('trace_events'); +const { + isStackOverflowError, + codes: { + ERR_CONSOLE_WRITABLE_STREAM, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + }, +} = require('internal/errors'); +const { previewEntries } = internalBinding('util'); +const { Buffer: { isBuffer } } = require('buffer'); +const util = require('util'); +const { + isTypedArray, isSet, isMap, isSetIterator, isMapIterator, +} = util.types; +const kCounts = Symbol('counts'); + +const kTraceConsoleCategory = 'node,node.console'; +const kTraceCount = 'C'.charCodeAt(0); +const kTraceBegin = 'b'.charCodeAt(0); +const kTraceEnd = 'e'.charCodeAt(0); +const kTraceInstant = 'n'.charCodeAt(0); + +const { + keys: ObjectKeys, + values: ObjectValues, +} = Object; +const hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); + +const { + isArray: ArrayIsArray, + from: ArrayFrom, +} = Array; + +// Lazy loaded for startup performance. +let cliTable; + +// Track amount of indentation required via `console.group()`. +const kGroupIndent = Symbol('kGroupIndent'); +const kFormatForStderr = Symbol('kFormatForStderr'); +const kFormatForStdout = Symbol('kFormatForStdout'); +const kGetInspectOptions = Symbol('kGetInspectOptions'); +const kColorMode = Symbol('kColorMode'); +const kIsConsole = Symbol('kIsConsole'); +const kWriteToConsole = Symbol('kWriteToConsole'); +const kBindProperties = Symbol('kBindProperties'); +const kBindStreamsEager = Symbol('kBindStreamsEager'); +const kBindStreamsLazy = Symbol('kBindStreamsLazy'); +const kUseStdout = Symbol('kUseStdout'); +const kUseStderr = Symbol('kUseStderr'); + +function Console(options /* or: stdout, stderr, ignoreErrors = true */) { + // We have to test new.target here to see if this function is called + // with new, because we need to define a custom instanceof to accommodate + // the global console. + if (!new.target) { + return new Console(...arguments); + } + + if (!options || typeof options.write === 'function') { + options = { + stdout: options, + stderr: arguments[1], + ignoreErrors: arguments[2] + }; + } + + const { + stdout, + stderr = stdout, + ignoreErrors = true, + colorMode = 'auto' + } = options; + + if (!stdout || typeof stdout.write !== 'function') { + throw new ERR_CONSOLE_WRITABLE_STREAM('stdout'); + } + if (!stderr || typeof stderr.write !== 'function') { + throw new ERR_CONSOLE_WRITABLE_STREAM('stderr'); + } + + if (typeof colorMode !== 'boolean' && colorMode !== 'auto') + throw new ERR_INVALID_ARG_VALUE('colorMode', colorMode); + + // bind the prototype functions to this Console instance + var keys = Object.keys(Console.prototype); + for (var v = 0; v < keys.length; v++) { + var k = keys[v]; + // We have to bind the methods grabbed from the instance instead of from + // the prototype so that users extending the Console can override them + // from the prototype chain of the subclass. + this[k] = this[k].bind(this); + } + + this[kBindStreamsEager](stdout, stderr); + this[kBindProperties](ignoreErrors, colorMode); +} + +const consolePropAttributes = { + writable: true, + enumerable: false, + configurable: true +}; + +// Fixup global.console instanceof global.console.Console +Object.defineProperty(Console, Symbol.hasInstance, { + value(instance) { + return instance[kIsConsole]; + } +}); + +// Eager version for the Console constructor +Console.prototype[kBindStreamsEager] = function(stdout, stderr) { + Object.defineProperties(this, { + '_stdout': { ...consolePropAttributes, value: stdout }, + '_stderr': { ...consolePropAttributes, value: stderr } + }); +}; + +// Lazily load the stdout and stderr from an object so we don't +// create the stdio streams when they are not even accessed +Console.prototype[kBindStreamsLazy] = function(object) { + let stdout; + let stderr; + Object.defineProperties(this, { + '_stdout': { + enumerable: false, + configurable: true, + get() { + if (!stdout) stdout = object.stdout; + return stdout; + }, + set(value) { stdout = value; } + }, + '_stderr': { + enumerable: false, + configurable: true, + get() { + if (!stderr) { stderr = object.stderr; } + return stderr; + }, + set(value) { stderr = value; } + } + }); +}; + +Console.prototype[kBindProperties] = function(ignoreErrors, colorMode) { + Object.defineProperties(this, { + '_stdoutErrorHandler': { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStdout) + }, + '_stderrErrorHandler': { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStderr) + }, + '_ignoreErrors': { + ...consolePropAttributes, + value: Boolean(ignoreErrors) + }, + '_times': { ...consolePropAttributes, value: new Map() } + }); + + // TODO(joyeecheung): use consolePropAttributes for these + // Corresponds to https://console.spec.whatwg.org/#count-map + this[kCounts] = new Map(); + this[kColorMode] = colorMode; + this[kIsConsole] = true; + this[kGroupIndent] = ''; +}; + +// Make a function that can serve as the callback passed to `stream.write()`. +function createWriteErrorHandler(instance, streamSymbol) { + return (err) => { + // This conditional evaluates to true if and only if there was an error + // that was not already emitted (which happens when the _write callback + // is invoked asynchronously). + const stream = streamSymbol === kUseStdout ? + instance._stdout : instance._stderr; + if (err !== null && !stream._writableState.errorEmitted) { + // If there was an error, it will be emitted on `stream` as + // an `error` event. Adding a `once` listener will keep that error + // from becoming an uncaught exception, but since the handler is + // removed after the event, non-console.* writes won't be affected. + // we are only adding noop if there is no one else listening for 'error' + if (stream.listenerCount('error') === 0) { + stream.on('error', noop); + } + } + }; +} + +Console.prototype[kWriteToConsole] = function(streamSymbol, string) { + const ignoreErrors = this._ignoreErrors; + const groupIndent = this[kGroupIndent]; + + const useStdout = streamSymbol === kUseStdout; + const stream = useStdout ? this._stdout : this._stderr; + const errorHandler = useStdout ? + this._stdoutErrorHandler : this._stderrErrorHandler; + + if (groupIndent.length !== 0) { + if (string.indexOf('\n') !== -1) { + string = string.replace(/\n/g, `\n${groupIndent}`); + } + string = groupIndent + string; + } + string += '\n'; + + if (ignoreErrors === false) return stream.write(string); + + // There may be an error occurring synchronously (e.g. for files or TTYs + // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so + // handle both situations. + try { + // Add and later remove a noop error handler to catch synchronous errors. + stream.once('error', noop); + + stream.write(string, errorHandler); + } catch (e) { + // console is a debugging utility, so it swallowing errors is not desirable + // even in edge cases such as low stack space. + if (isStackOverflowError(e)) + throw e; + // Sorry, there's no proper way to pass along the error here. + } finally { + stream.removeListener('error', noop); + } +}; + +const kColorInspectOptions = { colors: true }; +const kNoColorInspectOptions = {}; +Console.prototype[kGetInspectOptions] = function(stream) { + let color = this[kColorMode]; + if (color === 'auto') { + color = stream.isTTY && ( + typeof stream.getColorDepth === 'function' ? + stream.getColorDepth() > 2 : true); + } + + return color ? kColorInspectOptions : kNoColorInspectOptions; +}; + +Console.prototype[kFormatForStdout] = function(args) { + const opts = this[kGetInspectOptions](this._stdout); + return util.formatWithOptions(opts, ...args); +}; + +Console.prototype[kFormatForStderr] = function(args) { + const opts = this[kGetInspectOptions](this._stderr); + return util.formatWithOptions(opts, ...args); +}; + +Console.prototype.log = function log(...args) { + this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args)); +}; + +Console.prototype.debug = Console.prototype.log; +Console.prototype.info = Console.prototype.log; +Console.prototype.dirxml = Console.prototype.log; + +Console.prototype.warn = function warn(...args) { + this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); +}; + +Console.prototype.error = Console.prototype.warn; + +Console.prototype.dir = function dir(object, options) { + options = Object.assign({ + customInspect: false + }, this[kGetInspectOptions](this._stdout), options); + this[kWriteToConsole](kUseStdout, util.inspect(object, options)); +}; + +Console.prototype.time = function time(label = 'default') { + // Coerces everything other than Symbol to a string + label = `${label}`; + if (this._times.has(label)) { + process.emitWarning(`Label '${label}' already exists for console.time()`); + return; + } + trace(kTraceBegin, kTraceConsoleCategory, `time::${label}`, 0); + this._times.set(label, process.hrtime()); +}; + +Console.prototype.timeEnd = function timeEnd(label = 'default') { + // Coerces everything other than Symbol to a string + label = `${label}`; + const hasWarned = timeLogImpl(this, 'timeEnd', label); + trace(kTraceEnd, kTraceConsoleCategory, `time::${label}`, 0); + if (!hasWarned) { + this._times.delete(label); + } +}; + +Console.prototype.timeLog = function timeLog(label = 'default', ...data) { + // Coerces everything other than Symbol to a string + label = `${label}`; + timeLogImpl(this, 'timeLog', label, data); + trace(kTraceInstant, kTraceConsoleCategory, `time::${label}`, 0); +}; + +// Returns true if label was not found +function timeLogImpl(self, name, label, data) { + const time = self._times.get(label); + if (!time) { + process.emitWarning(`No such label '${label}' for console.${name}()`); + return true; + } + const duration = process.hrtime(time); + const ms = duration[0] * 1000 + duration[1] / 1e6; + if (data === undefined) { + self.log('%s: %sms', label, ms.toFixed(3)); + } else { + self.log('%s: %sms', label, ms.toFixed(3), ...data); + } + return false; +} + +Console.prototype.trace = function trace(...args) { + const err = { + name: 'Trace', + message: this[kFormatForStderr](args) + }; + Error.captureStackTrace(err, trace); + this.error(err.stack); +}; + +Console.prototype.assert = function assert(expression, ...args) { + if (!expression) { + args[0] = `Assertion failed${args.length === 0 ? '' : `: ${args[0]}`}`; + this.warn(...args); // the arguments will be formatted in warn() again + } +}; + +// Defined by: https://console.spec.whatwg.org/#clear +Console.prototype.clear = function clear() { + // It only makes sense to clear if _stdout is a TTY. + // Otherwise, do nothing. + if (this._stdout.isTTY) { + // The require is here intentionally to avoid readline being + // required too early when console is first loaded. + const { cursorTo, clearScreenDown } = require('readline'); + cursorTo(this._stdout, 0, 0); + clearScreenDown(this._stdout); + } +}; + +// Defined by: https://console.spec.whatwg.org/#count +Console.prototype.count = function count(label = 'default') { + // Ensures that label is a string, and only things that can be + // coerced to strings. e.g. Symbol is not allowed + label = `${label}`; + const counts = this[kCounts]; + let count = counts.get(label); + if (count === undefined) + count = 1; + else + count++; + counts.set(label, count); + trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, count); + this.log(`${label}: ${count}`); +}; + +// Defined by: https://console.spec.whatwg.org/#countreset +Console.prototype.countReset = function countReset(label = 'default') { + const counts = this[kCounts]; + if (!counts.has(label)) { + process.emitWarning(`Count for '${label}' does not exist`); + return; + } + trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, 0); + counts.delete(`${label}`); +}; + +Console.prototype.group = function group(...data) { + if (data.length > 0) { + this.log(...data); + } + this[kGroupIndent] += ' '; +}; +Console.prototype.groupCollapsed = Console.prototype.group; + +Console.prototype.groupEnd = function groupEnd() { + this[kGroupIndent] = + this[kGroupIndent].slice(0, this[kGroupIndent].length - 2); +}; + +const keyKey = 'Key'; +const valuesKey = 'Values'; +const indexKey = '(index)'; +const iterKey = '(iteration index)'; + +const isArray = (v) => ArrayIsArray(v) || isTypedArray(v) || isBuffer(v); + +// https://console.spec.whatwg.org/#table +Console.prototype.table = function(tabularData, properties) { + if (properties !== undefined && !ArrayIsArray(properties)) + throw new ERR_INVALID_ARG_TYPE('properties', 'Array', properties); + + if (tabularData === null || typeof tabularData !== 'object') + return this.log(tabularData); + + if (cliTable === undefined) cliTable = require('internal/cli_table'); + const final = (k, v) => this.log(cliTable(k, v)); + + const inspect = (v) => { + const opt = { depth: 0, maxArrayLength: 3 }; + if (v !== null && typeof v === 'object' && + !isArray(v) && ObjectKeys(v).length > 2) + opt.depth = -1; + Object.assign(opt, this[kGetInspectOptions](this._stdout)); + return util.inspect(v, opt); + }; + const getIndexArray = (length) => ArrayFrom({ length }, (_, i) => inspect(i)); + + const mapIter = isMapIterator(tabularData); + let isKeyValue = false; + let i = 0; + if (mapIter) { + const res = previewEntries(tabularData, true); + tabularData = res[0]; + isKeyValue = res[1]; + } + + if (isKeyValue || isMap(tabularData)) { + const keys = []; + const values = []; + let length = 0; + if (mapIter) { + for (; i < tabularData.length / 2; ++i) { + keys.push(inspect(tabularData[i * 2])); + values.push(inspect(tabularData[i * 2 + 1])); + length++; + } + } else { + for (const [k, v] of tabularData) { + keys.push(inspect(k)); + values.push(inspect(v)); + length++; + } + } + return final([ + iterKey, keyKey, valuesKey + ], [ + getIndexArray(length), + keys, + values, + ]); + } + + const setIter = isSetIterator(tabularData); + if (setIter) + tabularData = previewEntries(tabularData); + + const setlike = setIter || (mapIter && !isKeyValue) || isSet(tabularData); + if (setlike) { + const values = []; + let length = 0; + for (const v of tabularData) { + values.push(inspect(v)); + length++; + } + return final([setlike ? iterKey : indexKey, valuesKey], [ + getIndexArray(length), + values, + ]); + } + + const map = {}; + let hasPrimitives = false; + const valuesKeyArray = []; + const indexKeyArray = ObjectKeys(tabularData); + + for (; i < indexKeyArray.length; i++) { + const item = tabularData[indexKeyArray[i]]; + const primitive = item === null || + (typeof item !== 'function' && typeof item !== 'object'); + if (properties === undefined && primitive) { + hasPrimitives = true; + valuesKeyArray[i] = inspect(item); + } else { + const keys = properties || ObjectKeys(item); + for (const key of keys) { + if (map[key] === undefined) + map[key] = []; + if ((primitive && properties) || !hasOwnProperty(item, key)) + map[key][i] = ''; + else + map[key][i] = item == null ? item : inspect(item[key]); + } + } + } + + const keys = ObjectKeys(map); + const values = ObjectValues(map); + if (hasPrimitives) { + keys.push(valuesKey); + values.push(valuesKeyArray); + } + keys.unshift(indexKey); + values.unshift(indexKeyArray); + + return final(keys, values); +}; + +function noop() {} + +module.exports = { + Console, + kBindStreamsLazy, + kBindProperties +}; diff --git a/lib/internal/console/global.js b/lib/internal/console/global.js new file mode 100644 index 00000000000000..614941eba6da2d --- /dev/null +++ b/lib/internal/console/global.js @@ -0,0 +1,44 @@ +'use strict'; + +// See https://console.spec.whatwg.org/#console-namespace +// > For historical web-compatibility reasons, the namespace object +// > for console must have as its [[Prototype]] an empty object, +// > created as if by ObjectCreate(%ObjectPrototype%), +// > instead of %ObjectPrototype%. + +// Since in Node.js, the Console constructor has been exposed through +// require('console'), we need to keep the Console constructor but +// we cannot actually use `new Console` to construct the global console. +// Therefore, the console.Console.prototype is not +// in the global console prototype chain anymore. + +const { + Console, + kBindStreamsLazy, + kBindProperties +} = require('internal/console/constructor'); + +const globalConsole = Object.create({}); + +// Since Console is not on the prototype chain of the global console, +// the symbol properties on Console.prototype have to be looked up from +// the global console itself. In addition, we need to make the global +// console a namespace by binding the console methods directly onto +// the global console with the receiver fixed. +for (const prop of Reflect.ownKeys(Console.prototype)) { + if (prop === 'constructor') { continue; } + const desc = Reflect.getOwnPropertyDescriptor(Console.prototype, prop); + if (typeof desc.value === 'function') { // fix the receiver + desc.value = desc.value.bind(globalConsole); + } + Reflect.defineProperty(globalConsole, prop, desc); +} + +globalConsole[kBindStreamsLazy](process); +globalConsole[kBindProperties](true, 'auto'); + +// This is a legacy feature - the Console constructor is exposed on +// the global console instance. +globalConsole.Console = Console; + +module.exports = globalConsole; diff --git a/lib/internal/console/inspector.js b/lib/internal/console/inspector.js new file mode 100644 index 00000000000000..5e04289be9ae6a --- /dev/null +++ b/lib/internal/console/inspector.js @@ -0,0 +1,53 @@ +'use strict'; + +const path = require('path'); +const CJSModule = require('internal/modules/cjs/loader'); +const { makeRequireFunction } = require('internal/modules/cjs/helpers'); +const { tryGetCwd } = require('internal/util'); +const { addCommandLineAPI, consoleCall } = process.binding('inspector'); + +// Wrap a console implemented by Node.js with features from the VM inspector +function addInspectorApis(consoleFromNode, consoleFromVM) { + // Setup inspector command line API. + const cwd = tryGetCwd(path); + const consoleAPIModule = new CJSModule(''); + consoleAPIModule.paths = + CJSModule._nodeModulePaths(cwd).concat(CJSModule.globalPaths); + addCommandLineAPI('require', makeRequireFunction(consoleAPIModule)); + const config = {}; + + // If global console has the same method as inspector console, + // then wrap these two methods into one. Native wrapper will preserve + // the original stack. + for (const key of Object.keys(consoleFromNode)) { + if (!consoleFromVM.hasOwnProperty(key)) + continue; + consoleFromNode[key] = consoleCall.bind(consoleFromNode, + consoleFromVM[key], + consoleFromNode[key], + config); + } + + // Add additional console APIs from the inspector + for (const key of Object.keys(consoleFromVM)) { + if (consoleFromNode.hasOwnProperty(key)) + continue; + consoleFromNode[key] = consoleFromVM[key]; + } +} + +module.exports = { + addInspectorApis +}; + +// Stores the console from VM, should be set during bootstrap. +let consoleFromVM; + +Object.defineProperty(module.exports, 'consoleFromVM', { + get() { + return consoleFromVM; + }, + set(val) { + consoleFromVM = val; + } +}); diff --git a/lib/internal/util.js b/lib/internal/util.js index 3524b9e1d62112..414d037a298ca6 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -373,6 +373,17 @@ function once(callback) { }; } +function tryGetCwd(path) { + try { + return process.cwd(); + } catch { + // getcwd(3) can fail if the current working directory has been deleted. + // Fall back to the directory name of the (absolute) executable path. + // It's not really correct but what are the alternatives? + return path.dirname(process.execPath); + } +} + module.exports = { assertCrypto, cachedResult, @@ -392,6 +403,7 @@ module.exports = { once, promisify, spliceOne, + tryGetCwd, removeColors, // Symbol used to customize promisify conversion diff --git a/node.gyp b/node.gyp index 3cbbe61e575ca3..661727d9d58729 100644 --- a/node.gyp +++ b/node.gyp @@ -95,6 +95,9 @@ 'lib/internal/cluster/shared_handle.js', 'lib/internal/cluster/utils.js', 'lib/internal/cluster/worker.js', + 'lib/internal/console/constructor.js', + 'lib/internal/console/global.js', + 'lib/internal/console/inspector.js', 'lib/internal/crypto/certificate.js', 'lib/internal/crypto/cipher.js', 'lib/internal/crypto/diffiehellman.js', diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 272dec40060624..8f83194cbb1a52 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -9,7 +9,7 @@ const common = require('../common'); const assert = require('assert'); const isMainThread = common.isMainThread; -const kMaxModuleCount = isMainThread ? 56 : 78; +const kMaxModuleCount = isMainThread ? 58 : 80; assert(list.length <= kMaxModuleCount, `Total length: ${list.length}\n` + list.join('\n')