diff --git a/lib/classes/Error.js b/lib/classes/Error.js index 58be18fec..d5bb59625 100644 --- a/lib/classes/Error.js +++ b/lib/classes/Error.js @@ -1,32 +1,11 @@ 'use strict'; const chalk = require('chalk'); -const { inspect } = require('util'); -const { isError } = require('lodash'); -const slsVersion = require('./../../package').version; -const isStandaloneExecutable = require('../utils/isStandaloneExecutable'); -const sfeVersion = require('@serverless/enterprise-plugin/package.json').version; -const { sdkVersion } = require('@serverless/enterprise-plugin'); const consoleLog = (message) => { console.log(message); // eslint-disable-line no-console }; -const resolveExceptionMeta = (exception) => { - if (isError(exception)) { - return { - name: exception.name, - title: exception.name.replace(/([A-Z])/g, ' $1'), - stack: exception.stack, - message: exception.message, - }; - } - return { - title: 'Exception', - message: inspect(exception), - }; -}; - const writeMessage = (title, message) => { let line = ''; while (line.length < 56 - title.length) { @@ -62,75 +41,6 @@ module.exports.ServerlessError = ServerlessError; // Deprecated - use ServerlessError instead module.exports.SError = ServerlessError; -const userErrorNames = new Set(['ServerlessError', 'YAMLException']); - -module.exports.logError = (exception, { forceExit = false, serverless } = {}) => { - const exceptionMeta = resolveExceptionMeta(exception); - const isUserError = userErrorNames.has(exceptionMeta.name); - - const possiblyExit = () => { - if (forceExit) process.exit(); - }; - - writeMessage( - exceptionMeta.title, - exceptionMeta.stack && (!isUserError || process.env.SLS_DEBUG) - ? exceptionMeta.stack - : exceptionMeta.message - ); - - if (!isUserError && !process.env.SLS_DEBUG) { - const debugInfo = [ - ' ', - ' For debugging logs, run again after setting the', - ' "SLS_DEBUG=*" environment variable.', - ].join(''); - consoleLog(chalk.red(debugInfo)); - consoleLog(' '); - } - - const platform = process.platform; - const nodeVersion = process.version.replace(/^[v|V]/, ''); - - consoleLog(chalk.yellow(' Get Support --------------------------------------------')); - consoleLog(`${chalk.yellow(' Docs: ')}${'docs.serverless.com'}`); - consoleLog(`${chalk.yellow(' Bugs: ')}${'github.com/serverless/serverless/issues'}`); - consoleLog(`${chalk.yellow(' Issues: ')}${'forum.serverless.com'}`); - - consoleLog(' '); - consoleLog(chalk.yellow(' Your Environment Information ---------------------------')); - consoleLog(chalk.yellow(` Operating System: ${platform}`)); - consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`)); - - const installationModePostfix = (() => { - if (isStandaloneExecutable) return ' (standalone)'; - if (serverless && serverless.isLocallyInstalled) return ' (local)'; - return ''; - })(); - consoleLog( - chalk.yellow(` Framework Version: ${slsVersion}${installationModePostfix}`) - ); - consoleLog(chalk.yellow(` Plugin Version: ${sfeVersion}`)); - consoleLog(chalk.yellow(` SDK Version: ${sdkVersion}`)); - - // only show components version if user is running Node 8+ - const userNodeVersion = Number(process.version.split('.')[0].slice(1)); - if (userNodeVersion >= 8) { - const componentsVersion = (() => { - try { - return require('@serverless/components/package').version; - } catch (error) { - return 'Unavailable'; - } - })(); - consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`)); - } - consoleLog(' '); - - process.exitCode = 1; - possiblyExit(); -}; - module.exports.logWarning = (message) => { if (process.env.SLS_WARNING_DISABLE) { return; diff --git a/lib/cli/handle-error.js b/lib/cli/handle-error.js new file mode 100644 index 000000000..3b3d9b112 --- /dev/null +++ b/lib/cli/handle-error.js @@ -0,0 +1,110 @@ +'use strict'; + +const path = require('path'); +const { inspect } = require('util'); +const isError = require('type/error/is'); +const isObject = require('type/object/is'); +const chalk = require('chalk'); +const isStandaloneExecutable = require('../utils/isStandaloneExecutable'); +const resolveLocalServerlessPath = require('./resolve-local-serverless-path'); +const slsVersion = require('./../../package').version; +const sfeVersion = require('@serverless/enterprise-plugin/package.json').version; +const { sdkVersion } = require('@serverless/enterprise-plugin'); + +const userErrorNames = new Set(['ServerlessError', 'YAMLException']); +const serverlessPath = path.resolve(__dirname, '../Serverless.js'); + +const resolveExceptionMeta = (exception) => { + if (isError(exception)) { + return { + name: exception.name, + title: exception.name.replace(/([A-Z])/g, ' $1'), + stack: exception.stack, + message: exception.message, + }; + } + return { + title: 'Exception', + message: inspect(exception), + }; +}; + +const consoleLog = (message) => process.stdout.write(`${message}\n`); + +const writeMessage = (title, message) => { + let line = ''; + while (line.length < 56 - title.length) { + line = `${line}-`; + } + + process.stdout.write(' \n'); + consoleLog(chalk.yellow(` ${title} ${line}`)); + consoleLog(' '); + + if (message) { + consoleLog(` ${message.split('\n').join('\n ')}`); + } + + consoleLog(' '); +}; + +module.exports = async (exception, options = {}) => { + if (!isObject(options)) options = {}; + const { isUncaughtException, isLocallyInstalled } = options; + const exceptionMeta = resolveExceptionMeta(exception); + const isUserError = !isUncaughtException && userErrorNames.has(exceptionMeta.name); + + writeMessage( + exceptionMeta.title, + exceptionMeta.stack && (!isUserError || process.env.SLS_DEBUG) + ? exceptionMeta.stack + : exceptionMeta.message + ); + + if (!isUserError && !process.env.SLS_DEBUG) { + const debugInfo = [ + ' ', + ' For debugging logs, run again after setting the', + ' "SLS_DEBUG=*" environment variable.', + ].join(''); + consoleLog(chalk.red(debugInfo)); + consoleLog(' '); + } + + const platform = process.platform; + const nodeVersion = process.version.replace(/^[v|V]/, ''); + + consoleLog(chalk.yellow(' Get Support --------------------------------------------')); + consoleLog(`${chalk.yellow(' Docs: ')}docs.serverless.com`); + consoleLog(`${chalk.yellow(' Bugs: ')}github.com/serverless/serverless/issues`); + consoleLog(`${chalk.yellow(' Issues: ')}forum.serverless.com`); + + consoleLog(' '); + consoleLog(chalk.yellow(' Your Environment Information ---------------------------')); + consoleLog(chalk.yellow(` Operating System: ${platform}`)); + consoleLog(chalk.yellow(` Node Version: ${nodeVersion}`)); + + const installationModePostfix = await (async () => { + if (isStandaloneExecutable) return ' (standalone)'; + if (isLocallyInstalled != null) return isLocallyInstalled ? ' (local)' : ''; + return serverlessPath === (await resolveLocalServerlessPath()) ? ' (local)' : ''; + })(); + consoleLog( + chalk.yellow(` Framework Version: ${slsVersion}${installationModePostfix}`) + ); + consoleLog(chalk.yellow(` Plugin Version: ${sfeVersion}`)); + consoleLog(chalk.yellow(` SDK Version: ${sdkVersion}`)); + + const componentsVersion = (() => { + try { + return require('@serverless/components/package').version; + } catch (error) { + return 'Unavailable'; + } + })(); + consoleLog(chalk.yellow(` Components Version: ${componentsVersion}`)); + consoleLog(' '); + + process.exitCode = 1; + if (isUncaughtException) process.exit(); +}; diff --git a/scripts/serverless.js b/scripts/serverless.js index 30c924ce3..1f4d82544 100755 --- a/scripts/serverless.js +++ b/scripts/serverless.js @@ -13,56 +13,62 @@ if (require('../lib/utils/tabCompletion/isSupported') && process.argv[2] === 'co return; } -const logError = require('../lib/classes/Error').logError; +const handleError = require('../lib/cli/handle-error'); let serverless; -process.on('uncaughtException', (error) => logError(error, { forceExit: true, serverless })); +process.once('uncaughtException', (error) => + handleError(error, { + isUncaughtException: true, + isLocallyInstalled: serverless && serverless.isLocallyInstalled, + }) +); const processSpanPromise = (async () => { - const wait = require('timers-ext/promise/sleep'); - await wait(); // Ensure access to "processSpanPromise" - require('../lib/utils/analytics').sendPending({ - serverlessExecutionSpan: processSpanPromise, - }); - - const BbPromise = require('bluebird'); - const uuid = require('uuid'); - - const invocationId = uuid.v4(); - if (process.env.SLS_DEBUG) { - // For performance reasons enabled only in SLS_DEBUG mode - BbPromise.config({ - longStackTraces: true, - }); - } - - const Serverless = require('../lib/Serverless'); - serverless = new Serverless(); - try { - serverless.onExitPromise = processSpanPromise; - serverless.invocationId = invocationId; - await serverless.init(); - if (serverless.invokedInstance) serverless = serverless.invokedInstance; - await serverless.run(); - } catch (error) { - // If Enterprise Plugin, capture error - let enterpriseErrorHandler = null; - serverless.pluginManager.plugins.forEach((p) => { - if (p.enterprise && p.enterprise.errorHandler) { - enterpriseErrorHandler = p.enterprise.errorHandler; - } + const wait = require('timers-ext/promise/sleep'); + await wait(); // Ensure access to "processSpanPromise" + require('../lib/utils/analytics').sendPending({ + serverlessExecutionSpan: processSpanPromise, }); - if (!enterpriseErrorHandler) { - logError(error, { serverless }); - return; + + const BbPromise = require('bluebird'); + const uuid = require('uuid'); + + const invocationId = uuid.v4(); + if (process.env.SLS_DEBUG) { + // For performance reasons enabled only in SLS_DEBUG mode + BbPromise.config({ + longStackTraces: true, + }); } + + const Serverless = require('../lib/Serverless'); + serverless = new Serverless(); + try { - await enterpriseErrorHandler(error, invocationId); - } catch (enterpriseErrorHandlerError) { - process.stdout.write(`${enterpriseErrorHandlerError.stack}\n`); + serverless.onExitPromise = processSpanPromise; + serverless.invocationId = invocationId; + await serverless.init(); + if (serverless.invokedInstance) serverless = serverless.invokedInstance; + await serverless.run(); + } catch (error) { + // If Enterprise Plugin, capture error + let enterpriseErrorHandler = null; + serverless.pluginManager.plugins.forEach((p) => { + if (p.enterprise && p.enterprise.errorHandler) { + enterpriseErrorHandler = p.enterprise.errorHandler; + } + }); + if (!enterpriseErrorHandler) throw error; + try { + await enterpriseErrorHandler(error, invocationId); + } catch (enterpriseErrorHandlerError) { + process.stdout.write(`${enterpriseErrorHandlerError.stack}\n`); + } + throw error; } - logError(error, { serverless }); + } catch (error) { + handleError(error); } })(); diff --git a/test/unit/lib/classes/Error.test.js b/test/unit/lib/classes/Error.test.js index c8c071671..3384df5d2 100644 --- a/test/unit/lib/classes/Error.test.js +++ b/test/unit/lib/classes/Error.test.js @@ -2,9 +2,7 @@ const expect = require('chai').expect; const sandbox = require('sinon'); -const overrideEnv = require('process-utils/override-env'); const ServerlessError = require('../../../../lib/classes/Error').ServerlessError; -const logError = require('../../../../lib/classes/Error').logError; const logWarning = require('../../../../lib/classes/Error').logWarning; describe('ServerlessError', () => { @@ -53,7 +51,7 @@ describe('ServerlessError', () => { }); }); -describe('Error', () => { +describe('#logWarning()', () => { let consoleLogSpy; beforeEach(() => { @@ -64,117 +62,13 @@ describe('Error', () => { sandbox.restore(); }); - describe('#logError()', () => { - let restoreEnv; + it('should log warning and proceed', () => { + logWarning('a message'); - beforeEach(() => ({ restoreEnv } = overrideEnv())); + const message = consoleLogSpy.args.join('\n'); - afterEach(() => restoreEnv()); - - it('should log error and exit', () => { - const error = new ServerlessError('a message', 'a status code'); - logError(error); - - // TODO @David Not sure how to make async test for this - // If tracking enabled, the process exits in a callback and is not defined yet - // expect(this.processExitCodes.length).to.be.equal(1); - // expect(this.processExitCodes).gt(0); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.have.string('Serverless Error'); - expect(message).to.have.string('a message'); - }); - - it('should log environment information', () => { - const error = new ServerlessError('a message', 'a status code'); - logError(error); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - - expect(message).to.have.string('Serverless Error'); - expect(message).to.have.string('a message'); - expect(message).to.have.string('Your Environment Information'); - expect(message).to.have.string('Operating System:'); - expect(message).to.have.string('Node Version:'); - expect(message).to.have.string('Framework Version:'); - expect(message).to.have.string('Plugin Version:'); - expect(message).to.have.string('SDK Version:'); - }); - - it('should capture the exception and exit the process with 1 if errorReporter is setup', () => { - const error = new Error('an unexpected error'); - logError(error); - - expect(process.exitCode).to.equal(1); - }); - - it('should notify about SLS_DEBUG and ask report for unexpected errors', () => { - const error = new Error('an unexpected error'); - logError(error); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.have.string('SLS_DEBUG=*'); - }); - - it('should hide warnings if SLS_WARNING_DISABLE is defined', () => { - process.env.SLS_WARNING_DISABLE = '*'; - - logWarning('This is a warning'); - logWarning('This is another warning'); - logError(new Error('an error')); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.have.string('an error'); - expect(message).not.to.have.string('This is a warning'); - }); - - it('should print stack trace with SLS_DEBUG', () => { - process.env.SLS_DEBUG = '1'; - const error = new ServerlessError('a message'); - logError(error); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.have.string(error.stack.split('\n').join('\n ')); - }); - - it('should not print stack trace without SLS_DEBUG', () => { - const error = new ServerlessError('a message'); - logError(error); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.not.have.string('Stack Trace'); - expect(message).to.not.have.string(error.stack); - }); - - it('should handle non-error objects', () => { - logError('NON-ERROR INPUT'); - const message = consoleLogSpy.args.join('\n'); - - expect(message).to.have.string('NON-ERROR INPUT'); - }); - }); - - describe('#logWarning()', () => { - it('should log warning and proceed', () => { - logWarning('a message'); - - const message = consoleLogSpy.args.join('\n'); - - expect(consoleLogSpy.called).to.equal(true); - expect(message).to.have.string('Serverless Warning'); - expect(message).to.have.string('a message'); - }); + expect(consoleLogSpy.called).to.equal(true); + expect(message).to.have.string('Serverless Warning'); + expect(message).to.have.string('a message'); }); }); diff --git a/test/unit/lib/cli/handle-error.test.js b/test/unit/lib/cli/handle-error.test.js new file mode 100644 index 000000000..65b39da9b --- /dev/null +++ b/test/unit/lib/cli/handle-error.test.js @@ -0,0 +1,97 @@ +'use strict'; + +const { expect } = require('chai'); +const sinon = require('sinon'); + +const path = require('path'); +const overrideStdoutWrite = require('process-utils/override-stdout-write'); +const handleError = require('../../../../lib/cli/handle-error'); +const isStandaloneExecutable = require('../../../../lib/utils/isStandaloneExecutable'); +const { ServerlessError } = require('../../../../lib/classes/Error'); + +describe('test/unit/lib/cli/handle-error.test.js', () => { + it('should log environment information', async () => { + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error')) + ); + expect(stdoutData).to.have.string('Serverless Error'); + expect(stdoutData).to.have.string('Test error'); + expect(stdoutData).to.have.string('Your Environment Information'); + expect(stdoutData).to.have.string('Operating System:'); + expect(stdoutData).to.have.string('Node Version:'); + expect(stdoutData).to.have.string('Framework Version:'); + expect(stdoutData).to.have.string('Plugin Version:'); + expect(stdoutData).to.have.string('Components Version:'); + }); + + it('should support `isUncaughtException` option', async () => { + const processExitStub = sinon.stub(process, 'exit').returns(); + try { + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error'), { isUncaughtException: true }) + ); + expect(processExitStub.called).to.be.true; + } finally { + processExitStub.restore(); + } + }); + + if (isStandaloneExecutable) { + it('should report standalone installation', async () => { + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error')) + ); + expect(stdoutData).to.have.string('(standalone)'); + }); + } else { + it('should support `isLocallyInstalled` option', async () => { + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error'), { isLocallyInstalled: false }) + ); + expect(stdoutData).to.not.have.string('(local)'); + stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error'), { isLocallyInstalled: true }) + ); + expect(stdoutData).to.have.string('(local)'); + }); + } + + it('should print stack trace with SLS_DEBUG', async () => { + let stdoutData = ''; + process.env.SLS_DEBUG = '1'; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error')) + ); + expect(stdoutData).to.have.string(path.basename(__filename)); + }); + + it('should not print stack trace without SLS_DEBUG', async () => { + let stdoutData = ''; + delete process.env.SLS_DEBUG; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError(new ServerlessError('Test error')) + ); + expect(stdoutData).to.not.have.string(path.basename(__filename)); + }); + + it('should handle non-error objects', async () => { + let stdoutData = ''; + await overrideStdoutWrite( + (data) => (stdoutData += data), + () => handleError('NON-ERROR') + ); + expect(stdoutData).to.have.string('NON-ERROR'); + }); +});