diff --git a/lib/internal/bootstrap/node.js b/lib/internal/bootstrap/node.js index 870a54746b041b..a036ed9455a39c 100644 --- a/lib/internal/bootstrap/node.js +++ b/lib/internal/bootstrap/node.js @@ -19,7 +19,6 @@ const { internalBinding, NativeModule } = loaderExports; -const exceptionHandlerState = { captureFn: null }; let getOptionValue; function startup() { @@ -27,8 +26,23 @@ function startup() { setupProcessObject(); - // Do this good and early, since it handles errors. - setupProcessFatal(); + // TODO(joyeecheung): this does not have to done so early, any fatal errors + // thrown before user code execution should simply crash the process + // and we do not care about any clean up at that point. We don't care + // about emitting any events if the process crash upon bootstrap either. + { + const { + fatalException, + setUncaughtExceptionCaptureCallback, + hasUncaughtExceptionCaptureCallback + } = NativeModule.require('internal/process/execution'); + + process._fatalException = fatalException; + process.setUncaughtExceptionCaptureCallback = + setUncaughtExceptionCaptureCallback; + process.hasUncaughtExceptionCaptureCallback = + hasUncaughtExceptionCaptureCallback; + } setupGlobalVariables(); @@ -84,9 +98,7 @@ function startup() { process.reallyExit = rawMethods.reallyExit; process._kill = rawMethods._kill; - const wrapped = perThreadSetup.wrapProcessMethods( - rawMethods, exceptionHandlerState - ); + const wrapped = perThreadSetup.wrapProcessMethods(rawMethods); process._rawDebug = wrapped._rawDebug; process.hrtime = wrapped.hrtime; process.hrtime.bigint = wrapped.hrtimeBigInt; @@ -94,10 +106,6 @@ function startup() { process.memoryUsage = wrapped.memoryUsage; process.kill = wrapped.kill; process.exit = wrapped.exit; - process.setUncaughtExceptionCaptureCallback = - wrapped.setUncaughtExceptionCaptureCallback; - process.hasUncaughtExceptionCaptureCallback = - wrapped.hasUncaughtExceptionCaptureCallback; } NativeModule.require('internal/process/warning').setup(); @@ -133,9 +141,13 @@ function startup() { } if (isMainThread) { - mainThreadSetup.setupStdio(); + const { getStdout, getStdin, getStderr } = + NativeModule.require('internal/process/stdio').getMainThreadStdio(); + setupProcessStdio(getStdout, getStdin, getStderr); } else { - workerThreadSetup.setupStdio(); + const { getStdout, getStdin, getStderr } = + workerThreadSetup.initializeWorkerStdio(); + setupProcessStdio(getStdout, getStdin, getStderr); } if (global.__coverage__) @@ -287,8 +299,14 @@ function startup() { function startExecution() { // This means we are in a Worker context, and any script execution // will be directed by the worker module. - if (internalBinding('worker').getEnvMessagePort() !== undefined) { - NativeModule.require('internal/worker').setupChild(evalScript); + if (!isMainThread) { + const workerThreadSetup = NativeModule.require( + 'internal/process/worker_thread_only' + ); + // Set up the message port and start listening + const { workerFatalExeception } = workerThreadSetup.setup(); + // Overwrite fatalException + process._fatalException = workerFatalExeception; return; } @@ -359,7 +377,9 @@ function executeUserCode() { addBuiltinLibsToObject } = NativeModule.require('internal/modules/cjs/helpers'); addBuiltinLibsToObject(global); - evalScript('[eval]', wrapForBreakOnFirstLine(getOptionValue('--eval'))); + const source = getOptionValue('--eval'); + const { evalScript } = NativeModule.require('internal/process/execution'); + evalScript('[eval]', source, process._breakFirstLine); return; } @@ -413,7 +433,8 @@ function executeUserCode() { // User passed '-e' or '--eval' along with `-i` or `--interactive` if (process._eval != null) { - evalScript('[eval]', wrapForBreakOnFirstLine(process._eval)); + const { evalScript } = NativeModule.require('internal/process/execution'); + evalScript('[eval]', process._eval, process._breakFirstLine); } return; } @@ -435,7 +456,8 @@ function readAndExecuteStdin() { checkScriptSyntax(code, '[stdin]'); } else { process._eval = code; - evalScript('[stdin]', wrapForBreakOnFirstLine(process._eval)); + const { evalScript } = NativeModule.require('internal/process/execution'); + evalScript('[stdin]', process._eval, process._breakFirstLine); } }); } @@ -476,6 +498,31 @@ function setupProcessObject() { EventEmitter.call(process); } +function setupProcessStdio(getStdout, getStdin, getStderr) { + Object.defineProperty(process, 'stdout', { + configurable: true, + enumerable: true, + get: getStdout + }); + + Object.defineProperty(process, 'stderr', { + configurable: true, + enumerable: true, + get: getStderr + }); + + Object.defineProperty(process, 'stdin', { + configurable: true, + enumerable: true, + get: getStdin + }); + + process.openStdin = function() { + process.stdin.resume(); + return process.stdin; + }; +} + function setupGlobalVariables() { Object.defineProperty(global, Symbol.toStringTag, { value: 'global', @@ -639,95 +686,6 @@ function setupDOMException() { registerDOMException(DOMException); } -function noop() {} - -function setupProcessFatal() { - const { - executionAsyncId, - clearDefaultTriggerAsyncId, - clearAsyncIdStack, - hasAsyncIdStack, - afterHooksExist, - emitAfter - } = NativeModule.require('internal/async_hooks'); - - process._fatalException = (er) => { - // It's possible that defaultTriggerAsyncId was set for a constructor - // call that threw and was never cleared. So clear it now. - clearDefaultTriggerAsyncId(); - - if (exceptionHandlerState.captureFn !== null) { - exceptionHandlerState.captureFn(er); - } else if (!process.emit('uncaughtException', er)) { - // If someone handled it, then great. otherwise, die in C++ land - // since that means that we'll exit the process, emit the 'exit' event. - try { - if (!process._exiting) { - process._exiting = true; - process.exitCode = 1; - process.emit('exit', 1); - } - } catch { - // Nothing to be done about it at this point. - } - try { - const { kExpandStackSymbol } = NativeModule.require('internal/util'); - if (typeof er[kExpandStackSymbol] === 'function') - er[kExpandStackSymbol](); - } catch { - // Nothing to be done about it at this point. - } - return false; - } - - // If we handled an error, then make sure any ticks get processed - // by ensuring that the next Immediate cycle isn't empty. - NativeModule.require('timers').setImmediate(noop); - - // Emit the after() hooks now that the exception has been handled. - if (afterHooksExist()) { - do { - emitAfter(executionAsyncId()); - } while (hasAsyncIdStack()); - // Or completely empty the id stack. - } else { - clearAsyncIdStack(); - } - - return true; - }; -} - -function wrapForBreakOnFirstLine(source) { - if (!process._breakFirstLine) - return source; - const fn = `function() {\n\n${source};\n\n}`; - return `process.binding('inspector').callAndPauseOnStart(${fn}, {})`; -} - -function evalScript(name, body) { - 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); - module.filename = path.join(cwd, name); - module.paths = CJSModule._nodeModulePaths(cwd); - const script = `global.__filename = ${JSON.stringify(name)};\n` + - 'global.exports = exports;\n' + - 'global.module = module;\n' + - 'global.__dirname = __dirname;\n' + - 'global.require = require;\n' + - 'return require("vm").runInThisContext(' + - `${JSON.stringify(body)}, { filename: ` + - `${JSON.stringify(name)}, displayErrors: true });\n`; - const result = module._compile(script, `${name}-wrapper`); - if (getOptionValue('--print')) console.log(result); - // Handle any nextTicks added in the first tick of the program. - process._tickCallback(); -} - function checkScriptSyntax(source, filename) { const CJSModule = NativeModule.require('internal/modules/cjs/loader'); const vm = NativeModule.require('vm'); diff --git a/lib/internal/console/inspector.js b/lib/internal/console/inspector.js index d481896e8af4dc..3dbc68a193db53 100644 --- a/lib/internal/console/inspector.js +++ b/lib/internal/console/inspector.js @@ -1,15 +1,14 @@ '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 { tryGetCwd } = require('internal/process/execution'); const { addCommandLineAPI, consoleCall } = internalBinding('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 cwd = tryGetCwd(); const consoleAPIModule = new CJSModule(''); consoleAPIModule.paths = CJSModule._nodeModulePaths(cwd).concat(CJSModule.globalPaths); diff --git a/lib/internal/process/execution.js b/lib/internal/process/execution.js new file mode 100644 index 00000000000000..607829544a9115 --- /dev/null +++ b/lib/internal/process/execution.js @@ -0,0 +1,149 @@ +'use strict'; + +const path = require('path'); + +const { + codes: { + ERR_INVALID_ARG_TYPE, + ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET + } +} = require('internal/errors'); + +const { + executionAsyncId, + clearDefaultTriggerAsyncId, + clearAsyncIdStack, + hasAsyncIdStack, + afterHooksExist, + emitAfter +} = require('internal/async_hooks'); + +// shouldAbortOnUncaughtToggle is a typed array for faster +// communication with JS. +const { shouldAbortOnUncaughtToggle } = internalBinding('util'); + +function tryGetCwd() { + 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 evalScript(name, body, breakFristLine) { + const CJSModule = require('internal/modules/cjs/loader'); + if (breakFristLine) { + const fn = `function() {\n\n${body};\n\n}`; + body = `process.binding('inspector').callAndPauseOnStart(${fn}, {})`; + } + + const cwd = tryGetCwd(); + + const module = new CJSModule(name); + module.filename = path.join(cwd, name); + module.paths = CJSModule._nodeModulePaths(cwd); + const script = `global.__filename = ${JSON.stringify(name)};\n` + + 'global.exports = exports;\n' + + 'global.module = module;\n' + + 'global.__dirname = __dirname;\n' + + 'global.require = require;\n' + + 'return require("vm").runInThisContext(' + + `${JSON.stringify(body)}, { filename: ` + + `${JSON.stringify(name)}, displayErrors: true });\n`; + const result = module._compile(script, `${name}-wrapper`); + if (require('internal/options').getOptionValue('--print')) { + console.log(result); + } + // Handle any nextTicks added in the first tick of the program. + process._tickCallback(); +} + +const exceptionHandlerState = { captureFn: null }; + +function setUncaughtExceptionCaptureCallback(fn) { + if (fn === null) { + exceptionHandlerState.captureFn = fn; + shouldAbortOnUncaughtToggle[0] = 1; + return; + } + if (typeof fn !== 'function') { + throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn); + } + if (exceptionHandlerState.captureFn !== null) { + throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(); + } + exceptionHandlerState.captureFn = fn; + shouldAbortOnUncaughtToggle[0] = 0; +} + +function hasUncaughtExceptionCaptureCallback() { + return exceptionHandlerState.captureFn !== null; +} + +function noop() {} + +// XXX(joyeecheung): for some reason this cannot be defined at the top-level +// and exported to be written to process._fatalException, it has to be +// returned as an *anonymous function* wrapped inside a factory function, +// otherwise it breaks the test-timers.setInterval async hooks test - +// this may indicate that node::FatalException should fix up the callback scope +// before calling into process._fatalException, or this function should +// take extra care of the async hooks before it schedules a setImmediate. +function createFatalException() { + return (er) => { + // It's possible that defaultTriggerAsyncId was set for a constructor + // call that threw and was never cleared. So clear it now. + clearDefaultTriggerAsyncId(); + + if (exceptionHandlerState.captureFn !== null) { + exceptionHandlerState.captureFn(er); + } else if (!process.emit('uncaughtException', er)) { + // If someone handled it, then great. otherwise, die in C++ land + // since that means that we'll exit the process, emit the 'exit' event. + try { + if (!process._exiting) { + process._exiting = true; + process.exitCode = 1; + process.emit('exit', 1); + } + } catch { + // Nothing to be done about it at this point. + } + try { + const { kExpandStackSymbol } = require('internal/util'); + if (typeof er[kExpandStackSymbol] === 'function') + er[kExpandStackSymbol](); + } catch { + // Nothing to be done about it at this point. + } + return false; + } + + // If we handled an error, then make sure any ticks get processed + // by ensuring that the next Immediate cycle isn't empty. + require('timers').setImmediate(noop); + + // Emit the after() hooks now that the exception has been handled. + if (afterHooksExist()) { + do { + emitAfter(executionAsyncId()); + } while (hasAsyncIdStack()); + // Or completely empty the id stack. + } else { + clearAsyncIdStack(); + } + + return true; + }; +} + +module.exports = { + tryGetCwd, + evalScript, + fatalException: createFatalException(), + setUncaughtExceptionCaptureCallback, + hasUncaughtExceptionCaptureCallback +}; diff --git a/lib/internal/process/main_thread_only.js b/lib/internal/process/main_thread_only.js index 862194ae46e27e..42579e9da8acd1 100644 --- a/lib/internal/process/main_thread_only.js +++ b/lib/internal/process/main_thread_only.js @@ -16,15 +16,6 @@ const { validateString } = require('internal/validators'); -const { - setupProcessStdio, - getMainThreadStdio -} = require('internal/process/stdio'); - -function setupStdio() { - setupProcessStdio(getMainThreadStdio()); -} - // The execution of this function itself should not cause any side effects. function wrapProcessMethods(binding) { function chdir(directory) { @@ -174,7 +165,6 @@ function setupChildProcessIpcChannel() { } module.exports = { - setupStdio, wrapProcessMethods, setupSignalHandlers, setupChildProcessIpcChannel, diff --git a/lib/internal/process/per_thread.js b/lib/internal/process/per_thread.js index f1629e3a97c336..efa736fa619387 100644 --- a/lib/internal/process/per_thread.js +++ b/lib/internal/process/per_thread.js @@ -12,7 +12,6 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_OPT_VALUE, ERR_OUT_OF_RANGE, - ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET, ERR_UNKNOWN_SIGNAL } } = require('internal/errors'); @@ -24,7 +23,7 @@ function assert(x, msg) { } // The execution of this function itself should not cause any side effects. -function wrapProcessMethods(binding, exceptionHandlerState) { +function wrapProcessMethods(binding) { const { hrtime: _hrtime, hrtimeBigInt: _hrtimeBigInt, @@ -185,29 +184,6 @@ function wrapProcessMethods(binding, exceptionHandlerState) { return true; } - // shouldAbortOnUncaughtToggle is a typed array for faster - // communication with JS. - const { shouldAbortOnUncaughtToggle } = binding; - - function setUncaughtExceptionCaptureCallback(fn) { - if (fn === null) { - exceptionHandlerState.captureFn = fn; - shouldAbortOnUncaughtToggle[0] = 1; - return; - } - if (typeof fn !== 'function') { - throw new ERR_INVALID_ARG_TYPE('fn', ['Function', 'null'], fn); - } - if (exceptionHandlerState.captureFn !== null) { - throw new ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET(); - } - exceptionHandlerState.captureFn = fn; - shouldAbortOnUncaughtToggle[0] = 0; - } - - function hasUncaughtExceptionCaptureCallback() { - return exceptionHandlerState.captureFn !== null; - } return { _rawDebug, @@ -216,9 +192,7 @@ function wrapProcessMethods(binding, exceptionHandlerState) { cpuUsage, memoryUsage, kill, - exit, - setUncaughtExceptionCaptureCallback, - hasUncaughtExceptionCaptureCallback + exit }; } diff --git a/lib/internal/process/stdio.js b/lib/internal/process/stdio.js index 5e9ff6b26097a6..bf5f6df15f123c 100644 --- a/lib/internal/process/stdio.js +++ b/lib/internal/process/stdio.js @@ -1,6 +1,5 @@ 'use strict'; -exports.setupProcessStdio = setupProcessStdio; exports.getMainThreadStdio = getMainThreadStdio; function dummyDestroy(err, cb) { cb(err); } @@ -134,31 +133,6 @@ function getMainThreadStdio() { }; } -function setupProcessStdio({ getStdout, getStdin, getStderr }) { - Object.defineProperty(process, 'stdout', { - configurable: true, - enumerable: true, - get: getStdout - }); - - Object.defineProperty(process, 'stderr', { - configurable: true, - enumerable: true, - get: getStderr - }); - - Object.defineProperty(process, 'stdin', { - configurable: true, - enumerable: true, - get: getStdin - }); - - process.openStdin = function() { - process.stdin.resume(); - return process.stdin; - }; -} - function createWritableStdioStream(fd) { var stream; const tty_wrap = internalBinding('tty_wrap'); diff --git a/lib/internal/process/worker_thread_only.js b/lib/internal/process/worker_thread_only.js index 834ba6078fca44..d26159ab451c97 100644 --- a/lib/internal/process/worker_thread_only.js +++ b/lib/internal/process/worker_thread_only.js @@ -2,23 +2,135 @@ // This file contains process bootstrappers that can only be // run in the worker thread. - const { - setupProcessStdio -} = require('internal/process/stdio'); + getEnvMessagePort, + threadId +} = internalBinding('worker'); const { - workerStdio -} = require('internal/worker'); - -function setupStdio() { - setupProcessStdio({ - getStdout: () => workerStdio.stdout, - getStderr: () => workerStdio.stderr, - getStdin: () => workerStdio.stdin - }); + messageTypes, + kStdioWantsMoreDataCallback, + kWaitingStreams, + ReadableWorkerStdio, + WritableWorkerStdio +} = require('internal/worker/io'); + +let debuglog; +function debug(...args) { + if (!debuglog) { + debuglog = require('util').debuglog('worker'); + } + return debuglog(...args); +} + +const workerStdio = {}; + +function initializeWorkerStdio() { + const port = getEnvMessagePort(); + port[kWaitingStreams] = 0; + workerStdio.stdin = new ReadableWorkerStdio(port, 'stdin'); + workerStdio.stdout = new WritableWorkerStdio(port, 'stdout'); + workerStdio.stderr = new WritableWorkerStdio(port, 'stderr'); + + return { + getStdout() { return workerStdio.stdout; }, + getStderr() { return workerStdio.stderr; }, + getStdin() { return workerStdio.stdin; } + }; +} + +function createMessageHandler(port) { + const publicWorker = require('worker_threads'); + + return function(message) { + if (message.type === messageTypes.LOAD_SCRIPT) { + const { filename, doEval, workerData, publicPort, hasStdin } = message; + publicWorker.parentPort = publicPort; + publicWorker.workerData = workerData; + + if (!hasStdin) + workerStdio.stdin.push(null); + + debug(`[${threadId}] starts worker script ${filename} ` + + `(eval = ${eval}) at cwd = ${process.cwd()}`); + port.unref(); + port.postMessage({ type: messageTypes.UP_AND_RUNNING }); + if (doEval) { + const { evalScript } = require('internal/process/execution'); + evalScript('[worker eval]', filename); + } else { + process.argv[1] = filename; // script filename + require('module').runMain(); + } + return; + } else if (message.type === messageTypes.STDIO_PAYLOAD) { + const { stream, chunk, encoding } = message; + workerStdio[stream].push(chunk, encoding); + return; + } else if (message.type === messageTypes.STDIO_WANTS_MORE_DATA) { + const { stream } = message; + workerStdio[stream][kStdioWantsMoreDataCallback](); + return; + } + + require('assert').fail(`Unknown worker message type ${message.type}`); + }; +} + +// XXX(joyeecheung): this has to be returned as an anonymous function +// wrapped in a closure, see the comment of the original +// process._fatalException in lib/internal/process/execution.js +function createWorkerFatalExeception(port) { + const { + fatalException: originalFatalException + } = require('internal/process/execution'); + + return (error) => { + debug(`[${threadId}] gets fatal exception`); + let caught = false; + try { + caught = originalFatalException.call(this, error); + } catch (e) { + error = e; + } + debug(`[${threadId}] fatal exception caught = ${caught}`); + + if (!caught) { + let serialized; + try { + const { serializeError } = require('internal/error-serdes'); + serialized = serializeError(error); + } catch {} + debug(`[${threadId}] fatal exception serialized = ${!!serialized}`); + if (serialized) + port.postMessage({ + type: messageTypes.ERROR_MESSAGE, + error: serialized + }); + else + port.postMessage({ type: messageTypes.COULD_NOT_SERIALIZE_ERROR }); + + const { clearAsyncIdStack } = require('internal/async_hooks'); + clearAsyncIdStack(); + + process.exit(); + } + }; +} + +function setup() { + debug(`[${threadId}] is setting up worker child environment`); + + const port = getEnvMessagePort(); + port.on('message', createMessageHandler(port)); + port.start(); + + return { + workerFatalExeception: createWorkerFatalExeception(port) + }; } module.exports = { - setupStdio + initializeWorkerStdio, + setup }; diff --git a/lib/internal/util.js b/lib/internal/util.js index 7af06351c91406..3aa00fed35957f 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -379,17 +379,6 @@ 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, @@ -409,7 +398,6 @@ module.exports = { once, promisify, spliceOne, - tryGetCwd, removeColors, // Symbol used to customize promisify conversion diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 9e41dba7169236..2e1568a73822b9 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -4,7 +4,6 @@ const EventEmitter = require('events'); const assert = require('assert'); const path = require('path'); const util = require('util'); -const { Readable, Writable } = require('stream'); const { ERR_WORKER_PATH, ERR_WORKER_UNSERIALIZABLE_ERROR, @@ -12,27 +11,29 @@ const { } = require('internal/errors').codes; const { validateString } = require('internal/validators'); -const { MessagePort, MessageChannel } = internalBinding('messaging'); const { - handle_onclose: handleOnCloseSymbol, - oninit: onInitSymbol -} = internalBinding('symbols'); -const { clearAsyncIdStack } = require('internal/async_hooks'); -const { serializeError, deserializeError } = require('internal/error-serdes'); + drainMessagePort, + MessageChannel, + messageTypes, + kPort, + kIncrementsPortRef, + kWaitingStreams, + kStdioWantsMoreDataCallback, + setupPortReferencing, + ReadableWorkerStdio, + WritableWorkerStdio, +} = require('internal/worker/io'); +const { deserializeError } = require('internal/error-serdes'); const { pathToFileURL } = require('url'); const { Worker: WorkerImpl, - getEnvMessagePort, threadId } = internalBinding('worker'); const isMainThread = threadId === 0; -const kOnMessageListener = Symbol('kOnMessageListener'); const kHandle = Symbol('kHandle'); -const kName = Symbol('kName'); -const kPort = Symbol('kPort'); const kPublicPort = Symbol('kPublicPort'); const kDispose = Symbol('kDispose'); const kOnExit = Symbol('kOnExit'); @@ -40,213 +41,9 @@ const kOnMessage = Symbol('kOnMessage'); const kOnCouldNotSerializeErr = Symbol('kOnCouldNotSerializeErr'); const kOnErrorMessage = Symbol('kOnErrorMessage'); const kParentSideStdio = Symbol('kParentSideStdio'); -const kWritableCallbacks = Symbol('kWritableCallbacks'); -const kStdioWantsMoreDataCallback = Symbol('kStdioWantsMoreDataCallback'); -const kStartedReading = Symbol('kStartedReading'); -const kWaitingStreams = Symbol('kWaitingStreams'); -const kIncrementsPortRef = Symbol('kIncrementsPortRef'); const debug = util.debuglog('worker'); -const messageTypes = { - UP_AND_RUNNING: 'upAndRunning', - COULD_NOT_SERIALIZE_ERROR: 'couldNotSerializeError', - ERROR_MESSAGE: 'errorMessage', - STDIO_PAYLOAD: 'stdioPayload', - STDIO_WANTS_MORE_DATA: 'stdioWantsMoreData', - LOAD_SCRIPT: 'loadScript' -}; - -// We have to mess with the MessagePort prototype a bit, so that a) we can make -// it inherit from EventEmitter, even though it is a C++ class, and b) we do -// not provide methods that are not present in the Browser and not documented -// on our side (e.g. hasRef). -// Save a copy of the original set of methods as a shallow clone. -const MessagePortPrototype = Object.create( - Object.getPrototypeOf(MessagePort.prototype), - Object.getOwnPropertyDescriptors(MessagePort.prototype)); -// Set up the new inheritance chain. -Object.setPrototypeOf(MessagePort, EventEmitter); -Object.setPrototypeOf(MessagePort.prototype, EventEmitter.prototype); -// Finally, purge methods we don't want to be public. -delete MessagePort.prototype.stop; -delete MessagePort.prototype.drain; -MessagePort.prototype.ref = MessagePortPrototype.ref; -MessagePort.prototype.unref = MessagePortPrototype.unref; - -// A communication channel consisting of a handle (that wraps around an -// uv_async_t) which can receive information from other threads and emits -// .onmessage events, and a function used for sending data to a MessagePort -// in some other thread. -MessagePort.prototype[kOnMessageListener] = function onmessage(payload) { - debug(`[${threadId}] received message`, payload); - // Emit the deserialized object to userland. - this.emit('message', payload); -}; - -// This is for compatibility with the Web's MessagePort API. It makes sense to -// provide it as an `EventEmitter` in Node.js, but if somebody overrides -// `onmessage`, we'll switch over to the Web API model. -Object.defineProperty(MessagePort.prototype, 'onmessage', { - enumerable: true, - configurable: true, - get() { - return this[kOnMessageListener]; - }, - set(value) { - this[kOnMessageListener] = value; - if (typeof value === 'function') { - this.ref(); - MessagePortPrototype.start.call(this); - } else { - this.unref(); - MessagePortPrototype.stop.call(this); - } - } -}); - -// This is called from inside the `MessagePort` constructor. -function oninit() { - setupPortReferencing(this, this, 'message'); -} - -Object.defineProperty(MessagePort.prototype, onInitSymbol, { - enumerable: true, - writable: false, - value: oninit -}); - -// This is called after the underlying `uv_async_t` has been closed. -function onclose() { - if (typeof this.onclose === 'function') { - // Not part of the Web standard yet, but there aren't many reasonable - // alternatives in a non-EventEmitter usage setting. - // Refs: https://github.com/whatwg/html/issues/1766 - this.onclose(); - } - this.emit('close'); -} - -Object.defineProperty(MessagePort.prototype, handleOnCloseSymbol, { - enumerable: false, - writable: false, - value: onclose -}); - -MessagePort.prototype.close = function(cb) { - if (typeof cb === 'function') - this.once('close', cb); - MessagePortPrototype.close.call(this); -}; - -Object.defineProperty(MessagePort.prototype, util.inspect.custom, { - enumerable: false, - writable: false, - value: function inspect() { // eslint-disable-line func-name-matching - let ref; - try { - // This may throw when `this` does not refer to a native object, - // e.g. when accessing the prototype directly. - ref = MessagePortPrototype.hasRef.call(this); - } catch { return this; } - return Object.assign(Object.create(MessagePort.prototype), - ref === undefined ? { - active: false, - } : { - active: true, - refed: ref - }, - this); - } -}); - -function setupPortReferencing(port, eventEmitter, eventName) { - // Keep track of whether there are any workerMessage listeners: - // If there are some, ref() the channel so it keeps the event loop alive. - // If there are none or all are removed, unref() the channel so the worker - // can shutdown gracefully. - port.unref(); - eventEmitter.on('newListener', (name) => { - if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { - port.ref(); - MessagePortPrototype.start.call(port); - } - }); - eventEmitter.on('removeListener', (name) => { - if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { - MessagePortPrototype.stop.call(port); - port.unref(); - } - }); -} - - -class ReadableWorkerStdio extends Readable { - constructor(port, name) { - super(); - this[kPort] = port; - this[kName] = name; - this[kIncrementsPortRef] = true; - this[kStartedReading] = false; - this.on('end', () => { - if (this[kIncrementsPortRef] && --this[kPort][kWaitingStreams] === 0) - this[kPort].unref(); - }); - } - - _read() { - if (!this[kStartedReading] && this[kIncrementsPortRef]) { - this[kStartedReading] = true; - if (this[kPort][kWaitingStreams]++ === 0) - this[kPort].ref(); - } - - this[kPort].postMessage({ - type: messageTypes.STDIO_WANTS_MORE_DATA, - stream: this[kName] - }); - } -} - -class WritableWorkerStdio extends Writable { - constructor(port, name) { - super({ decodeStrings: false }); - this[kPort] = port; - this[kName] = name; - this[kWritableCallbacks] = []; - } - - _write(chunk, encoding, cb) { - this[kPort].postMessage({ - type: messageTypes.STDIO_PAYLOAD, - stream: this[kName], - chunk, - encoding - }); - this[kWritableCallbacks].push(cb); - if (this[kPort][kWaitingStreams]++ === 0) - this[kPort].ref(); - } - - _final(cb) { - this[kPort].postMessage({ - type: messageTypes.STDIO_PAYLOAD, - stream: this[kName], - chunk: null - }); - cb(); - } - - [kStdioWantsMoreDataCallback]() { - const cbs = this[kWritableCallbacks]; - this[kWritableCallbacks] = []; - for (const cb of cbs) - cb(); - if ((this[kPort][kWaitingStreams] -= cbs.length) === 0) - this[kPort].unref(); - } -} - class Worker extends EventEmitter { constructor(filename, options = {}) { super(); @@ -314,8 +111,8 @@ class Worker extends EventEmitter { [kOnExit](code) { debug(`[${threadId}] hears end event for Worker ${this.threadId}`); - MessagePortPrototype.drain.call(this[kPublicPort]); - MessagePortPrototype.drain.call(this[kPort]); + drainMessagePort(this[kPublicPort]); + drainMessagePort(this[kPort]); this[kDispose](); this.emit('exit', code); this.removeAllListeners(); @@ -421,92 +218,6 @@ class Worker extends EventEmitter { } } -const workerStdio = {}; -if (!isMainThread) { - const port = getEnvMessagePort(); - port[kWaitingStreams] = 0; - workerStdio.stdin = new ReadableWorkerStdio(port, 'stdin'); - workerStdio.stdout = new WritableWorkerStdio(port, 'stdout'); - workerStdio.stderr = new WritableWorkerStdio(port, 'stderr'); -} - -let originalFatalException; - -function setupChild(evalScript) { - // Called during bootstrap to set up worker script execution. - debug(`[${threadId}] is setting up worker child environment`); - const port = getEnvMessagePort(); - - const publicWorker = require('worker_threads'); - - port.on('message', (message) => { - if (message.type === messageTypes.LOAD_SCRIPT) { - const { filename, doEval, workerData, publicPort, hasStdin } = message; - publicWorker.parentPort = publicPort; - publicWorker.workerData = workerData; - - if (!hasStdin) - workerStdio.stdin.push(null); - - debug(`[${threadId}] starts worker script ${filename} ` + - `(eval = ${eval}) at cwd = ${process.cwd()}`); - port.unref(); - port.postMessage({ type: messageTypes.UP_AND_RUNNING }); - if (doEval) { - evalScript('[worker eval]', filename); - } else { - process.argv[1] = filename; // script filename - require('module').runMain(); - } - return; - } else if (message.type === messageTypes.STDIO_PAYLOAD) { - const { stream, chunk, encoding } = message; - workerStdio[stream].push(chunk, encoding); - return; - } else if (message.type === messageTypes.STDIO_WANTS_MORE_DATA) { - const { stream } = message; - workerStdio[stream][kStdioWantsMoreDataCallback](); - return; - } - - assert.fail(`Unknown worker message type ${message.type}`); - }); - - port.start(); - - originalFatalException = process._fatalException; - process._fatalException = fatalException; - - function fatalException(error) { - debug(`[${threadId}] gets fatal exception`); - let caught = false; - try { - caught = originalFatalException.call(this, error); - } catch (e) { - error = e; - } - debug(`[${threadId}] fatal exception caught = ${caught}`); - - if (!caught) { - let serialized; - try { - serialized = serializeError(error); - } catch {} - debug(`[${threadId}] fatal exception serialized = ${!!serialized}`); - if (serialized) - port.postMessage({ - type: messageTypes.ERROR_MESSAGE, - error: serialized - }); - else - port.postMessage({ type: messageTypes.COULD_NOT_SERIALIZE_ERROR }); - clearAsyncIdStack(); - - process.exit(); - } - } -} - function pipeWithoutWarning(source, dest) { const sourceMaxListeners = source._maxListeners; const destMaxListeners = dest._maxListeners; @@ -520,11 +231,7 @@ function pipeWithoutWarning(source, dest) { } module.exports = { - MessagePort, - MessageChannel, threadId, Worker, - setupChild, - isMainThread, - workerStdio + isMainThread }; diff --git a/lib/internal/worker/io.js b/lib/internal/worker/io.js new file mode 100644 index 00000000000000..d249ba8508b7af --- /dev/null +++ b/lib/internal/worker/io.js @@ -0,0 +1,245 @@ +'use strict'; + +const { + handle_onclose: handleOnCloseSymbol, + oninit: onInitSymbol +} = internalBinding('symbols'); +const { + MessagePort, + MessageChannel +} = internalBinding('messaging'); +const { threadId } = internalBinding('worker'); + +const { Readable, Writable } = require('stream'); +const EventEmitter = require('events'); +const util = require('util'); +const debug = util.debuglog('worker'); + +const kIncrementsPortRef = Symbol('kIncrementsPortRef'); +const kName = Symbol('kName'); +const kOnMessageListener = Symbol('kOnMessageListener'); +const kPort = Symbol('kPort'); +const kWaitingStreams = Symbol('kWaitingStreams'); +const kWritableCallbacks = Symbol('kWritableCallbacks'); +const kStartedReading = Symbol('kStartedReading'); +const kStdioWantsMoreDataCallback = Symbol('kStdioWantsMoreDataCallback'); + +const messageTypes = { + UP_AND_RUNNING: 'upAndRunning', + COULD_NOT_SERIALIZE_ERROR: 'couldNotSerializeError', + ERROR_MESSAGE: 'errorMessage', + STDIO_PAYLOAD: 'stdioPayload', + STDIO_WANTS_MORE_DATA: 'stdioWantsMoreData', + LOAD_SCRIPT: 'loadScript' +}; + +// Original drain from C++ +const originalDrain = MessagePort.prototype.drain; + +function drainMessagePort(port) { + return originalDrain.call(port); +} + +// We have to mess with the MessagePort prototype a bit, so that a) we can make +// it inherit from EventEmitter, even though it is a C++ class, and b) we do +// not provide methods that are not present in the Browser and not documented +// on our side (e.g. hasRef). +// Save a copy of the original set of methods as a shallow clone. +const MessagePortPrototype = Object.create( + Object.getPrototypeOf(MessagePort.prototype), + Object.getOwnPropertyDescriptors(MessagePort.prototype)); +// Set up the new inheritance chain. +Object.setPrototypeOf(MessagePort, EventEmitter); +Object.setPrototypeOf(MessagePort.prototype, EventEmitter.prototype); +// Finally, purge methods we don't want to be public. +delete MessagePort.prototype.stop; +delete MessagePort.prototype.drain; +MessagePort.prototype.ref = MessagePortPrototype.ref; +MessagePort.prototype.unref = MessagePortPrototype.unref; + +// A communication channel consisting of a handle (that wraps around an +// uv_async_t) which can receive information from other threads and emits +// .onmessage events, and a function used for sending data to a MessagePort +// in some other thread. +MessagePort.prototype[kOnMessageListener] = function onmessage(payload) { + debug(`[${threadId}] received message`, payload); + // Emit the deserialized object to userland. + this.emit('message', payload); +}; + +// This is for compatibility with the Web's MessagePort API. It makes sense to +// provide it as an `EventEmitter` in Node.js, but if somebody overrides +// `onmessage`, we'll switch over to the Web API model. +Object.defineProperty(MessagePort.prototype, 'onmessage', { + enumerable: true, + configurable: true, + get() { + return this[kOnMessageListener]; + }, + set(value) { + this[kOnMessageListener] = value; + if (typeof value === 'function') { + this.ref(); + MessagePortPrototype.start.call(this); + } else { + this.unref(); + MessagePortPrototype.stop.call(this); + } + } +}); + +// This is called from inside the `MessagePort` constructor. +function oninit() { + setupPortReferencing(this, this, 'message'); +} + +Object.defineProperty(MessagePort.prototype, onInitSymbol, { + enumerable: true, + writable: false, + value: oninit +}); + +// This is called after the underlying `uv_async_t` has been closed. +function onclose() { + if (typeof this.onclose === 'function') { + // Not part of the Web standard yet, but there aren't many reasonable + // alternatives in a non-EventEmitter usage setting. + // Refs: https://github.com/whatwg/html/issues/1766 + this.onclose(); + } + this.emit('close'); +} + +Object.defineProperty(MessagePort.prototype, handleOnCloseSymbol, { + enumerable: false, + writable: false, + value: onclose +}); + +MessagePort.prototype.close = function(cb) { + if (typeof cb === 'function') + this.once('close', cb); + MessagePortPrototype.close.call(this); +}; + +Object.defineProperty(MessagePort.prototype, util.inspect.custom, { + enumerable: false, + writable: false, + value: function inspect() { // eslint-disable-line func-name-matching + let ref; + try { + // This may throw when `this` does not refer to a native object, + // e.g. when accessing the prototype directly. + ref = MessagePortPrototype.hasRef.call(this); + } catch { return this; } + return Object.assign(Object.create(MessagePort.prototype), + ref === undefined ? { + active: false, + } : { + active: true, + refed: ref + }, + this); + } +}); + +function setupPortReferencing(port, eventEmitter, eventName) { + // Keep track of whether there are any workerMessage listeners: + // If there are some, ref() the channel so it keeps the event loop alive. + // If there are none or all are removed, unref() the channel so the worker + // can shutdown gracefully. + port.unref(); + eventEmitter.on('newListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + port.ref(); + MessagePortPrototype.start.call(port); + } + }); + eventEmitter.on('removeListener', (name) => { + if (name === eventName && eventEmitter.listenerCount(eventName) === 0) { + MessagePortPrototype.stop.call(port); + port.unref(); + } + }); +} + + +class ReadableWorkerStdio extends Readable { + constructor(port, name) { + super(); + this[kPort] = port; + this[kName] = name; + this[kIncrementsPortRef] = true; + this[kStartedReading] = false; + this.on('end', () => { + if (this[kIncrementsPortRef] && --this[kPort][kWaitingStreams] === 0) + this[kPort].unref(); + }); + } + + _read() { + if (!this[kStartedReading] && this[kIncrementsPortRef]) { + this[kStartedReading] = true; + if (this[kPort][kWaitingStreams]++ === 0) + this[kPort].ref(); + } + + this[kPort].postMessage({ + type: messageTypes.STDIO_WANTS_MORE_DATA, + stream: this[kName] + }); + } +} + +class WritableWorkerStdio extends Writable { + constructor(port, name) { + super({ decodeStrings: false }); + this[kPort] = port; + this[kName] = name; + this[kWritableCallbacks] = []; + } + + _write(chunk, encoding, cb) { + this[kPort].postMessage({ + type: messageTypes.STDIO_PAYLOAD, + stream: this[kName], + chunk, + encoding + }); + this[kWritableCallbacks].push(cb); + if (this[kPort][kWaitingStreams]++ === 0) + this[kPort].ref(); + } + + _final(cb) { + this[kPort].postMessage({ + type: messageTypes.STDIO_PAYLOAD, + stream: this[kName], + chunk: null + }); + cb(); + } + + [kStdioWantsMoreDataCallback]() { + const cbs = this[kWritableCallbacks]; + this[kWritableCallbacks] = []; + for (const cb of cbs) + cb(); + if ((this[kPort][kWaitingStreams] -= cbs.length) === 0) + this[kPort].unref(); + } +} + +module.exports = { + drainMessagePort, + messageTypes, + kPort, + kIncrementsPortRef, + kWaitingStreams, + kStdioWantsMoreDataCallback, + MessagePort, + MessageChannel, + setupPortReferencing, + ReadableWorkerStdio, + WritableWorkerStdio +}; diff --git a/lib/worker_threads.js b/lib/worker_threads.js index 0609650cd5731d..828edb6bffce7b 100644 --- a/lib/worker_threads.js +++ b/lib/worker_threads.js @@ -2,12 +2,15 @@ const { isMainThread, - MessagePort, - MessageChannel, threadId, Worker } = require('internal/worker'); +const { + MessagePort, + MessageChannel +} = require('internal/worker/io'); + module.exports = { isMainThread, MessagePort, diff --git a/node.gyp b/node.gyp index 8560c1d512ca14..296a51f55eaf1e 100644 --- a/node.gyp +++ b/node.gyp @@ -140,6 +140,7 @@ 'lib/internal/print_help.js', 'lib/internal/priority_queue.js', 'lib/internal/process/esm_loader.js', + 'lib/internal/process/execution.js', 'lib/internal/process/main_thread_only.js', 'lib/internal/process/next_tick.js', 'lib/internal/process/per_thread.js', @@ -178,6 +179,7 @@ 'lib/internal/stream_base_commons.js', 'lib/internal/vm/source_text_module.js', 'lib/internal/worker.js', + 'lib/internal/worker/io.js', 'lib/internal/streams/lazy_transform.js', 'lib/internal/streams/async_iterator.js', 'lib/internal/streams/buffer_list.js', diff --git a/src/node_process.cc b/src/node_process.cc index 477ac2adc8eb6c..867d08916a258b 100644 --- a/src/node_process.cc +++ b/src/node_process.cc @@ -458,14 +458,6 @@ static void InitializeProcessMethods(Local target, env->SetMethod(target, "dlopen", binding::DLOpen); env->SetMethod(target, "reallyExit", Exit); env->SetMethodNoSideEffect(target, "uptime", Uptime); - - Local should_abort_on_uncaught_toggle = - FIXED_ONE_BYTE_STRING(env->isolate(), "shouldAbortOnUncaughtToggle"); - CHECK(target - ->Set(env->context(), - should_abort_on_uncaught_toggle, - env->should_abort_on_uncaught_toggle().GetJSArray()) - .FromJust()); } } // namespace node diff --git a/src/node_util.cc b/src/node_util.cc index f7412d92bcd8e7..44d2ab1364de2f 100644 --- a/src/node_util.cc +++ b/src/node_util.cc @@ -232,6 +232,14 @@ void Initialize(Local target, target->Set(context, FIXED_ONE_BYTE_STRING(env->isolate(), "propertyFilter"), constants).FromJust(); + + Local should_abort_on_uncaught_toggle = + FIXED_ONE_BYTE_STRING(env->isolate(), "shouldAbortOnUncaughtToggle"); + CHECK(target + ->Set(env->context(), + should_abort_on_uncaught_toggle, + env->should_abort_on_uncaught_toggle().GetJSArray()) + .FromJust()); } } // namespace util diff --git a/test/message/eval_messages.out b/test/message/eval_messages.out index dfa8ec0f7d400b..6bfca9ea32d044 100644 --- a/test/message/eval_messages.out +++ b/test/message/eval_messages.out @@ -8,7 +8,7 @@ SyntaxError: Strict mode code may not include a with statement at Object.runInThisContext (vm.js:*:*) at Object. ([eval]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) at startup (internal/bootstrap/node.js:*:*) @@ -25,7 +25,7 @@ Error: hello at Object.runInThisContext (vm.js:*:*) at Object. ([eval]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) at startup (internal/bootstrap/node.js:*:*) @@ -41,7 +41,7 @@ Error: hello at Object.runInThisContext (vm.js:*:*) at Object. ([eval]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) at startup (internal/bootstrap/node.js:*:*) @@ -57,7 +57,7 @@ ReferenceError: y is not defined at Object.runInThisContext (vm.js:*:*) at Object. ([eval]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at executeUserCode (internal/bootstrap/node.js:*:*) at startExecution (internal/bootstrap/node.js:*:*) at startup (internal/bootstrap/node.js:*:*) diff --git a/test/message/stdin_messages.out b/test/message/stdin_messages.out index 6ec340c0c42cd3..ac1a4df02816df 100644 --- a/test/message/stdin_messages.out +++ b/test/message/stdin_messages.out @@ -8,7 +8,7 @@ SyntaxError: Strict mode code may not include a with statement at Object.runInThisContext (vm.js:*) at Object. ([stdin]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at Socket.process.stdin.on (internal/bootstrap/node.js:*:*) at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) @@ -25,7 +25,7 @@ Error: hello at Object.runInThisContext (vm.js:*) at Object. ([stdin]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at Socket.process.stdin.on (internal/bootstrap/node.js:*:*) at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) @@ -40,7 +40,7 @@ Error: hello at Object.runInThisContext (vm.js:*) at Object. ([stdin]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at Socket.process.stdin.on (internal/bootstrap/node.js:*:*) at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) @@ -56,7 +56,7 @@ ReferenceError: y is not defined at Object.runInThisContext (vm.js:*) at Object. ([stdin]-wrapper:*:*) at Module._compile (internal/modules/cjs/loader.js:*:*) - at evalScript (internal/bootstrap/node.js:*:*) + at evalScript (internal/process/execution.js:*:*) at Socket.process.stdin.on (internal/bootstrap/node.js:*:*) at Socket.emit (events.js:*:*) at endReadableNT (_stream_readable.js:*:*) diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index ee4864e09ba1d5..1fa5a643f6e308 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 ? 62 : 84; +const kMaxModuleCount = isMainThread ? 63 : 85; assert(list.length <= kMaxModuleCount, `Total length: ${list.length}\n` + list.join('\n')