From 7e62d5f7e2d34bcefd9416e1876d58501bb51a48 Mon Sep 17 00:00:00 2001 From: PG-practice Date: Mon, 23 Sep 2024 14:07:35 +0000 Subject: [PATCH 1/4] add error_handler.ts and tests --- packages/create-amplify/src/create_amplify.ts | 19 +- .../create-amplify/src/error_handler.test.ts | 204 ++++++++++++++++++ packages/create-amplify/src/error_handler.ts | 148 +++++++++++++ 3 files changed, 363 insertions(+), 8 deletions(-) create mode 100644 packages/create-amplify/src/error_handler.test.ts create mode 100644 packages/create-amplify/src/error_handler.ts diff --git a/packages/create-amplify/src/create_amplify.ts b/packages/create-amplify/src/create_amplify.ts index 3608268be7f..1312747c617 100644 --- a/packages/create-amplify/src/create_amplify.ts +++ b/packages/create-amplify/src/create_amplify.ts @@ -7,17 +7,19 @@ If customers have a cached version of the create-amplify package, they might execute that cached version even after we publish features and fixes to the package on npm. */ -import { - LogLevel, - PackageManagerControllerFactory, - format, - printer, -} from '@aws-amplify/cli-core'; +import { PackageManagerControllerFactory, format } from '@aws-amplify/cli-core'; import { ProjectRootValidator } from './project_root_validator.js'; import { AmplifyProjectCreator } from './amplify_project_creator.js'; import { getProjectRoot } from './get_project_root.js'; import { GitIgnoreInitializer } from './gitignore_initializer.js'; import { InitialProjectFileGenerator } from './initial_project_file_generator.js'; +import { + attachUnhandledExceptionListeners, + generateCommandFailureHandler, +} from './error_handler.js'; + +attachUnhandledExceptionListeners(); +const errorHandler = generateCommandFailureHandler(); const projectRoot = await getProjectRoot(); @@ -39,6 +41,7 @@ const amplifyProjectCreator = new AmplifyProjectCreator( try { await amplifyProjectCreator.create(); } catch (err) { - printer.log(format.error(err), LogLevel.ERROR); - process.exitCode = 1; + if (err instanceof Error) { + await errorHandler(format.error(err), err); + } } diff --git a/packages/create-amplify/src/error_handler.test.ts b/packages/create-amplify/src/error_handler.test.ts new file mode 100644 index 00000000000..1f980f9c933 --- /dev/null +++ b/packages/create-amplify/src/error_handler.test.ts @@ -0,0 +1,204 @@ +import { after, before, beforeEach, describe, it, mock } from 'node:test'; +import { + attachUnhandledExceptionListeners, + generateCommandFailureHandler, +} from './error_handler.js'; +import { LogLevel, printer } from '@aws-amplify/cli-core'; +import assert from 'node:assert'; +import { AmplifyUserError } from '@aws-amplify/platform-core'; + +const mockPrint = mock.method(printer, 'print'); +const mockLog = mock.method(printer, 'log'); + +void describe('generateCommandFailureHandler', () => { + beforeEach(() => { + mockPrint.mock.resetCalls(); + mockLog.mock.resetCalls(); + }); + + void it('prints specified message with undefined error', async () => { + const someMsg = 'some msg'; + // undefined error is encountered with --help option. + await generateCommandFailureHandler()( + someMsg, + undefined as unknown as Error + ); + assert.equal(mockPrint.mock.callCount(), 1); + assert.match(mockPrint.mock.calls[0].arguments[0], new RegExp(someMsg)); + }); + + void it('prints message from error object', async () => { + const errMsg = 'some error msg'; + await generateCommandFailureHandler()('', new Error(errMsg)); + assert.equal(mockPrint.mock.callCount(), 1); + assert.match( + mockPrint.mock.calls[0].arguments[0] as string, + new RegExp(errMsg) + ); + }); + + void it('handles a prompt force close error', async () => { + await generateCommandFailureHandler()( + '', + new Error('User force closed the prompt') + ); + assert.equal(mockPrint.mock.callCount(), 0); + }); + + void it('prints error cause message, if any', async () => { + const errorMessage = 'this is the upstream cause'; + await generateCommandFailureHandler()( + '', + new Error('some error msg', { cause: new Error(errorMessage) }) + ); + assert.equal(mockPrint.mock.callCount(), 2); + assert.match( + mockPrint.mock.calls[1].arguments[0] as string, + new RegExp(errorMessage) + ); + }); + + void it('prints AmplifyErrors', async () => { + await generateCommandFailureHandler()( + '', + new AmplifyUserError('TestNameError', { + message: 'test error message', + resolution: 'test resolution', + details: 'test details', + }) + ); + + assert.equal(mockPrint.mock.callCount(), 3); + assert.match( + mockPrint.mock.calls[0].arguments[0], + /TestNameError: test error message/ + ); + assert.equal( + mockPrint.mock.calls[1].arguments[0], + 'Resolution: test resolution' + ); + assert.equal(mockPrint.mock.calls[2].arguments[0], 'Details: test details'); + }); + + void it('prints debug stack traces', async () => { + const causeError = new Error('test underlying cause error'); + const amplifyError = new AmplifyUserError( + 'TestNameError', + { + message: 'test error message', + resolution: 'test resolution', + details: 'test details', + }, + causeError + ); + await generateCommandFailureHandler()('', amplifyError); + assert.equal(mockLog.mock.callCount(), 2); + assert.deepStrictEqual(mockLog.mock.calls[0].arguments, [ + amplifyError.stack, + LogLevel.DEBUG, + ]); + assert.deepStrictEqual(mockLog.mock.calls[1].arguments, [ + causeError.stack, + LogLevel.DEBUG, + ]); + }); +}); + +void describe( + 'attachUnhandledExceptionListeners', + { concurrency: 1 }, + async () => { + before(async () => { + attachUnhandledExceptionListeners(); + }); + + beforeEach(() => { + mockPrint.mock.resetCalls(); + }); + + after(() => { + // remove the exception listeners that were added during setup + process.listeners('unhandledRejection').pop(); + process.listeners('uncaughtException').pop(); + }); + void it('handles rejected errors', () => { + process.listeners('unhandledRejection').at(-1)?.( + new Error('test error'), + Promise.resolve() + ); + assert.ok( + mockPrint.mock.calls.findIndex((call) => + call.arguments[0].includes('test error') + ) >= 0 + ); + expectProcessExitCode1AndReset(); + }); + + void it('handles rejected strings', () => { + process.listeners('unhandledRejection').at(-1)?.( + 'test error', + Promise.resolve() + ); + assert.ok( + mockPrint.mock.calls.findIndex((call) => + call.arguments[0].includes('test error') + ) >= 0 + ); + expectProcessExitCode1AndReset(); + }); + + void it('handles rejected symbols of other types', () => { + process.listeners('unhandledRejection').at(-1)?.( + { something: 'weird' }, + Promise.resolve() + ); + assert.ok( + mockPrint.mock.calls.findIndex((call) => + call.arguments[0].includes( + 'Error: Unhandled rejection of type [object]' + ) + ) >= 0 + ); + expectProcessExitCode1AndReset(); + }); + + void it('handles uncaught errors', () => { + process.listeners('uncaughtException').at(-1)?.( + new Error('test error'), + 'uncaughtException' + ); + assert.ok( + mockPrint.mock.calls.findIndex((call) => + call.arguments[0].includes('test error') + ) >= 0 + ); + expectProcessExitCode1AndReset(); + }); + + void it('does nothing when called multiple times', () => { + // note the first call happened in the before() setup + + const unhandledRejectionListenerCount = + process.listenerCount('unhandledRejection'); + const uncaughtExceptionListenerCount = + process.listenerCount('uncaughtException'); + + attachUnhandledExceptionListeners(); + attachUnhandledExceptionListeners(); + + assert.equal( + process.listenerCount('unhandledRejection'), + unhandledRejectionListenerCount + ); + assert.equal( + process.listenerCount('uncaughtException'), + uncaughtExceptionListenerCount + ); + }); + } +); + +const expectProcessExitCode1AndReset = () => { + assert.equal(process.exitCode, 1); + process.exitCode = 0; +}; diff --git a/packages/create-amplify/src/error_handler.ts b/packages/create-amplify/src/error_handler.ts new file mode 100644 index 00000000000..23ae09c8c81 --- /dev/null +++ b/packages/create-amplify/src/error_handler.ts @@ -0,0 +1,148 @@ +import { LogLevel, format, printer } from '@aws-amplify/cli-core'; +import { AmplifyError } from '@aws-amplify/platform-core'; + +let hasAttachUnhandledExceptionListenersBeenCalled = false; + +type HandleErrorProps = { + error?: Error; + printMessagePreamble?: () => void; + message?: string; +}; + +/** + * Attaches process listeners to handle unhandled exceptions and rejections + */ +export const attachUnhandledExceptionListeners = (): void => { + if (hasAttachUnhandledExceptionListenersBeenCalled) { + return; + } + process.on('unhandledRejection', (reason) => { + process.exitCode = 1; + if (reason instanceof Error) { + void handleErrorSafe({ error: reason }); + } else if (typeof reason === 'string') { + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors + void handleErrorSafe({ error: new Error(reason) }); + } else { + void handleErrorSafe({ + // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors + error: new Error(`Unhandled rejection of type [${typeof reason}]`, { + cause: reason, + }), + }); + } + }); + + process.on('uncaughtException', (error) => { + process.exitCode = 1; + void handleErrorSafe({ error }); + }); + hasAttachUnhandledExceptionListenersBeenCalled = true; +}; + +/** + * Generates a function that is intended to be used as a callback to yargs.fail() + * All logic for actually handling errors should be delegated to handleError. + */ +export const generateCommandFailureHandler = (): (( + message: string, + error: Error +) => Promise) => { + /** + * Format error output when a command fails + * @param message error message + * @param error error + */ + const handleCommandFailure = async (message: string, error?: Error) => { + await handleErrorSafe({ + error, + message, + }); + }; + return handleCommandFailure; +}; + +const handleErrorSafe = async (props: HandleErrorProps) => { + try { + await handleError(props); + } catch (e) { + printer.log(format.error(e), LogLevel.DEBUG); + // no-op should gracefully exit + return; + } +}; + +/** + * Error handling for uncaught errors during CLI command execution. + * + * This should be the one and only place where we handle unexpected errors. + * This includes console logging, debug logging, metrics recording, etc. + * (Note that we don't do all of those things yet, but this is where they should go) + */ +const handleError = async ({ + error, + printMessagePreamble, + message, +}: HandleErrorProps) => { + // If yargs threw an error because the customer force-closed a prompt (ie Ctrl+C during a prompt) then the intent to exit the process is clear + if (isUserForceClosePromptError(error)) { + return; + } + + printMessagePreamble?.(); + + if (error instanceof AmplifyError) { + printer.print(format.error(`${error.name}: ${error.message}`)); + + if (error.resolution) { + printer.print(`Resolution: ${error.resolution}`); + } + if (error.details) { + printer.print(`Details: ${error.details}`); + } + if (errorHasCauseMessage(error)) { + printer.print(`Cause: ${error.cause.message}`); + } + } else { + // non-Amplify Error object + printer.print(format.error(message || String(error))); + + if (errorHasCauseMessage(error)) { + printer.print(`Cause: ${error.cause.message}`); + } + } + + // additional debug logging for the stack traces + if (error?.stack) { + printer.log(error.stack, LogLevel.DEBUG); + } + if (errorHasCauseStackTrace(error)) { + printer.log(error.cause.stack, LogLevel.DEBUG); + } +}; + +const isUserForceClosePromptError = (err?: Error): boolean => { + return !!err && err?.message.includes('User force closed the prompt'); +}; + +const errorHasCauseStackTrace = ( + error?: Error +): error is Error & { cause: { stack: string } } => { + return ( + typeof error?.cause === 'object' && + !!error.cause && + 'stack' in error.cause && + typeof error.cause.stack === 'string' + ); +}; + +const errorHasCauseMessage = ( + error?: Error +): error is Error & { cause: { message: string } } => { + return ( + typeof error?.cause === 'object' && + !!error.cause && + 'message' in error.cause && + typeof error.cause.message === 'string' + ); +}; From 3919c589d48e7f9a2e7ffe0150f15b7cdbacc709 Mon Sep 17 00:00:00 2001 From: PG-practice Date: Mon, 23 Sep 2024 14:09:56 +0000 Subject: [PATCH 2/4] add changeset --- .changeset/happy-students-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/happy-students-stare.md diff --git a/.changeset/happy-students-stare.md b/.changeset/happy-students-stare.md new file mode 100644 index 00000000000..a61aba1f509 --- /dev/null +++ b/.changeset/happy-students-stare.md @@ -0,0 +1,5 @@ +--- +'create-amplify': patch +--- + +add process handler to gracefully exit when Ctrl + C. See Issue #825. From dcaa3ae7be6ffd97ed58aa83c25bf31d0612fb26 Mon Sep 17 00:00:00 2001 From: PG-practice Date: Tue, 14 Jan 2025 14:52:23 +0000 Subject: [PATCH 3/4] Consolidate error_handler.ts --- packages/cli-core/package.json | 3 +- .../src/error_handler.test.ts | 3 +- .../{cli => cli-core}/src/error_handler.ts | 46 ++-- .../src/extract_sub_commands.ts | 0 packages/cli-core/src/index.ts | 2 + packages/cli/src/ampx.ts | 13 +- packages/cli/src/test-utils/command_runner.ts | 6 +- packages/create-amplify/src/create_amplify.ts | 11 +- .../create-amplify/src/error_handler.test.ts | 204 ------------------ packages/create-amplify/src/error_handler.ts | 148 ------------- 10 files changed, 53 insertions(+), 383 deletions(-) rename packages/{cli => cli-core}/src/error_handler.test.ts (98%) rename packages/{cli => cli-core}/src/error_handler.ts (87%) rename packages/{cli => cli-core}/src/extract_sub_commands.ts (100%) delete mode 100644 packages/create-amplify/src/error_handler.test.ts delete mode 100644 packages/create-amplify/src/error_handler.ts diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index 266bbd5932a..4eee7390c2c 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -23,6 +23,7 @@ "@inquirer/prompts": "^3.0.0", "execa": "^9.5.1", "kleur": "^4.1.5", - "zod": "^3.22.2" + "zod": "^3.22.2", + "yargs": "^17.7.2" } } diff --git a/packages/cli/src/error_handler.test.ts b/packages/cli-core/src/error_handler.test.ts similarity index 98% rename from packages/cli/src/error_handler.test.ts rename to packages/cli-core/src/error_handler.test.ts index 2285878283a..3548de1622e 100644 --- a/packages/cli/src/error_handler.test.ts +++ b/packages/cli-core/src/error_handler.test.ts @@ -4,7 +4,8 @@ import { generateCommandFailureHandler, } from './error_handler.js'; import { Argv } from 'yargs'; -import { LogLevel, printer } from '@aws-amplify/cli-core'; +import { LogLevel } from './printer/printer.js'; +import { printer } from './printer.js'; import assert from 'node:assert'; import { AmplifyUserError, UsageDataEmitter } from '@aws-amplify/platform-core'; diff --git a/packages/cli/src/error_handler.ts b/packages/cli-core/src/error_handler.ts similarity index 87% rename from packages/cli/src/error_handler.ts rename to packages/cli-core/src/error_handler.ts index 806020e9861..53b5c5db888 100644 --- a/packages/cli/src/error_handler.ts +++ b/packages/cli-core/src/error_handler.ts @@ -1,4 +1,7 @@ -import { LogLevel, format, printer } from '@aws-amplify/cli-core'; +import { LogLevel } from './printer/printer.js'; +import { format } from './format/format.js'; +import { printer } from './printer.js'; + import { Argv } from 'yargs'; import { AmplifyError, UsageDataEmitter } from '@aws-amplify/platform-core'; import { extractSubCommands } from './extract_sub_commands.js'; @@ -17,7 +20,7 @@ type HandleErrorProps = { * Attaches process listeners to handle unhandled exceptions and rejections */ export const attachUnhandledExceptionListeners = ( - usageDataEmitter: UsageDataEmitter + usageDataEmitter?: UsageDataEmitter ): void => { if (hasAttachUnhandledExceptionListenersBeenCalled) { return; @@ -54,7 +57,7 @@ export const attachUnhandledExceptionListeners = ( * This prevents our top-level error handler from being invoked after the yargs error handler has already been invoked */ export const generateCommandFailureHandler = ( - parser: Argv, + parser?: Argv, usageDataEmitter?: UsageDataEmitter ): ((message: string, error: Error) => Promise) => { /** @@ -63,19 +66,30 @@ export const generateCommandFailureHandler = ( * @param error error thrown by yargs handler */ const handleCommandFailure = async (message: string, error?: Error) => { - const printHelp = () => { - printer.printNewLine(); - parser.showHelp(); - printer.printNewLine(); - }; - await handleErrorSafe({ - command: extractSubCommands(parser), - printMessagePreamble: printHelp, - error, - message, - usageDataEmitter, - }); - parser.exit(1, error || new Error(message)); + // create-amplify command + if (!parser) { + await handleErrorSafe({ + error, + message, + }); + } + + // for ampx commands + if (parser) { + const printHelp = () => { + printer.printNewLine(); + parser.showHelp(); + printer.printNewLine(); + }; + await handleErrorSafe({ + command: extractSubCommands(parser), + printMessagePreamble: printHelp, + error, + message, + usageDataEmitter, + }); + parser.exit(1, error || new Error(message)); + } }; return handleCommandFailure; }; diff --git a/packages/cli/src/extract_sub_commands.ts b/packages/cli-core/src/extract_sub_commands.ts similarity index 100% rename from packages/cli/src/extract_sub_commands.ts rename to packages/cli-core/src/extract_sub_commands.ts diff --git a/packages/cli-core/src/index.ts b/packages/cli-core/src/index.ts index e9057b6ee42..4b04e5abc8e 100644 --- a/packages/cli-core/src/index.ts +++ b/packages/cli-core/src/index.ts @@ -3,3 +3,5 @@ export * from './printer/printer.js'; export * from './printer.js'; export { ColorName, colorNames, format, Format } from './format/format.js'; export * from './package-manager-controller/package_manager_controller_factory.js'; +export * from './error_handler.js'; +export * from './extract_sub_commands.js'; diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts index c9448a58d1d..773b291b2bf 100755 --- a/packages/cli/src/ampx.ts +++ b/packages/cli/src/ampx.ts @@ -1,10 +1,5 @@ #!/usr/bin/env node import { createMainParser } from './main_parser_factory.js'; -import { - attachUnhandledExceptionListeners, - generateCommandFailureHandler, -} from './error_handler.js'; -import { extractSubCommands } from './extract_sub_commands.js'; import { AmplifyFault, PackageJsonReader, @@ -13,7 +8,13 @@ import { import { fileURLToPath } from 'node:url'; import { verifyCommandName } from './verify_command_name.js'; import { hideBin } from 'yargs/helpers'; -import { PackageManagerControllerFactory, format } from '@aws-amplify/cli-core'; +import { + PackageManagerControllerFactory, + attachUnhandledExceptionListeners, + extractSubCommands, + format, + generateCommandFailureHandler, +} from '@aws-amplify/cli-core'; const packageJson = new PackageJsonReader().read( fileURLToPath(new URL('../package.json', import.meta.url)) diff --git a/packages/cli/src/test-utils/command_runner.ts b/packages/cli/src/test-utils/command_runner.ts index 823190bd086..d9cf2fc7fda 100644 --- a/packages/cli/src/test-utils/command_runner.ts +++ b/packages/cli/src/test-utils/command_runner.ts @@ -1,8 +1,10 @@ import { Argv } from 'yargs'; import { AsyncLocalStorage } from 'node:async_hooks'; import { UsageDataEmitter } from '@aws-amplify/platform-core'; -import { generateCommandFailureHandler } from '../error_handler.js'; -import { extractSubCommands } from '../extract_sub_commands.js'; +import { + extractSubCommands, + generateCommandFailureHandler, +} from '@aws-amplify/cli-core'; class OutputInterceptor { private output = ''; diff --git a/packages/create-amplify/src/create_amplify.ts b/packages/create-amplify/src/create_amplify.ts index 1312747c617..5459bf4fcd1 100644 --- a/packages/create-amplify/src/create_amplify.ts +++ b/packages/create-amplify/src/create_amplify.ts @@ -7,16 +7,17 @@ If customers have a cached version of the create-amplify package, they might execute that cached version even after we publish features and fixes to the package on npm. */ -import { PackageManagerControllerFactory, format } from '@aws-amplify/cli-core'; +import { + PackageManagerControllerFactory, + attachUnhandledExceptionListeners, + format, + generateCommandFailureHandler, +} from '@aws-amplify/cli-core'; import { ProjectRootValidator } from './project_root_validator.js'; import { AmplifyProjectCreator } from './amplify_project_creator.js'; import { getProjectRoot } from './get_project_root.js'; import { GitIgnoreInitializer } from './gitignore_initializer.js'; import { InitialProjectFileGenerator } from './initial_project_file_generator.js'; -import { - attachUnhandledExceptionListeners, - generateCommandFailureHandler, -} from './error_handler.js'; attachUnhandledExceptionListeners(); const errorHandler = generateCommandFailureHandler(); diff --git a/packages/create-amplify/src/error_handler.test.ts b/packages/create-amplify/src/error_handler.test.ts deleted file mode 100644 index 1f980f9c933..00000000000 --- a/packages/create-amplify/src/error_handler.test.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { after, before, beforeEach, describe, it, mock } from 'node:test'; -import { - attachUnhandledExceptionListeners, - generateCommandFailureHandler, -} from './error_handler.js'; -import { LogLevel, printer } from '@aws-amplify/cli-core'; -import assert from 'node:assert'; -import { AmplifyUserError } from '@aws-amplify/platform-core'; - -const mockPrint = mock.method(printer, 'print'); -const mockLog = mock.method(printer, 'log'); - -void describe('generateCommandFailureHandler', () => { - beforeEach(() => { - mockPrint.mock.resetCalls(); - mockLog.mock.resetCalls(); - }); - - void it('prints specified message with undefined error', async () => { - const someMsg = 'some msg'; - // undefined error is encountered with --help option. - await generateCommandFailureHandler()( - someMsg, - undefined as unknown as Error - ); - assert.equal(mockPrint.mock.callCount(), 1); - assert.match(mockPrint.mock.calls[0].arguments[0], new RegExp(someMsg)); - }); - - void it('prints message from error object', async () => { - const errMsg = 'some error msg'; - await generateCommandFailureHandler()('', new Error(errMsg)); - assert.equal(mockPrint.mock.callCount(), 1); - assert.match( - mockPrint.mock.calls[0].arguments[0] as string, - new RegExp(errMsg) - ); - }); - - void it('handles a prompt force close error', async () => { - await generateCommandFailureHandler()( - '', - new Error('User force closed the prompt') - ); - assert.equal(mockPrint.mock.callCount(), 0); - }); - - void it('prints error cause message, if any', async () => { - const errorMessage = 'this is the upstream cause'; - await generateCommandFailureHandler()( - '', - new Error('some error msg', { cause: new Error(errorMessage) }) - ); - assert.equal(mockPrint.mock.callCount(), 2); - assert.match( - mockPrint.mock.calls[1].arguments[0] as string, - new RegExp(errorMessage) - ); - }); - - void it('prints AmplifyErrors', async () => { - await generateCommandFailureHandler()( - '', - new AmplifyUserError('TestNameError', { - message: 'test error message', - resolution: 'test resolution', - details: 'test details', - }) - ); - - assert.equal(mockPrint.mock.callCount(), 3); - assert.match( - mockPrint.mock.calls[0].arguments[0], - /TestNameError: test error message/ - ); - assert.equal( - mockPrint.mock.calls[1].arguments[0], - 'Resolution: test resolution' - ); - assert.equal(mockPrint.mock.calls[2].arguments[0], 'Details: test details'); - }); - - void it('prints debug stack traces', async () => { - const causeError = new Error('test underlying cause error'); - const amplifyError = new AmplifyUserError( - 'TestNameError', - { - message: 'test error message', - resolution: 'test resolution', - details: 'test details', - }, - causeError - ); - await generateCommandFailureHandler()('', amplifyError); - assert.equal(mockLog.mock.callCount(), 2); - assert.deepStrictEqual(mockLog.mock.calls[0].arguments, [ - amplifyError.stack, - LogLevel.DEBUG, - ]); - assert.deepStrictEqual(mockLog.mock.calls[1].arguments, [ - causeError.stack, - LogLevel.DEBUG, - ]); - }); -}); - -void describe( - 'attachUnhandledExceptionListeners', - { concurrency: 1 }, - async () => { - before(async () => { - attachUnhandledExceptionListeners(); - }); - - beforeEach(() => { - mockPrint.mock.resetCalls(); - }); - - after(() => { - // remove the exception listeners that were added during setup - process.listeners('unhandledRejection').pop(); - process.listeners('uncaughtException').pop(); - }); - void it('handles rejected errors', () => { - process.listeners('unhandledRejection').at(-1)?.( - new Error('test error'), - Promise.resolve() - ); - assert.ok( - mockPrint.mock.calls.findIndex((call) => - call.arguments[0].includes('test error') - ) >= 0 - ); - expectProcessExitCode1AndReset(); - }); - - void it('handles rejected strings', () => { - process.listeners('unhandledRejection').at(-1)?.( - 'test error', - Promise.resolve() - ); - assert.ok( - mockPrint.mock.calls.findIndex((call) => - call.arguments[0].includes('test error') - ) >= 0 - ); - expectProcessExitCode1AndReset(); - }); - - void it('handles rejected symbols of other types', () => { - process.listeners('unhandledRejection').at(-1)?.( - { something: 'weird' }, - Promise.resolve() - ); - assert.ok( - mockPrint.mock.calls.findIndex((call) => - call.arguments[0].includes( - 'Error: Unhandled rejection of type [object]' - ) - ) >= 0 - ); - expectProcessExitCode1AndReset(); - }); - - void it('handles uncaught errors', () => { - process.listeners('uncaughtException').at(-1)?.( - new Error('test error'), - 'uncaughtException' - ); - assert.ok( - mockPrint.mock.calls.findIndex((call) => - call.arguments[0].includes('test error') - ) >= 0 - ); - expectProcessExitCode1AndReset(); - }); - - void it('does nothing when called multiple times', () => { - // note the first call happened in the before() setup - - const unhandledRejectionListenerCount = - process.listenerCount('unhandledRejection'); - const uncaughtExceptionListenerCount = - process.listenerCount('uncaughtException'); - - attachUnhandledExceptionListeners(); - attachUnhandledExceptionListeners(); - - assert.equal( - process.listenerCount('unhandledRejection'), - unhandledRejectionListenerCount - ); - assert.equal( - process.listenerCount('uncaughtException'), - uncaughtExceptionListenerCount - ); - }); - } -); - -const expectProcessExitCode1AndReset = () => { - assert.equal(process.exitCode, 1); - process.exitCode = 0; -}; diff --git a/packages/create-amplify/src/error_handler.ts b/packages/create-amplify/src/error_handler.ts deleted file mode 100644 index 23ae09c8c81..00000000000 --- a/packages/create-amplify/src/error_handler.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { LogLevel, format, printer } from '@aws-amplify/cli-core'; -import { AmplifyError } from '@aws-amplify/platform-core'; - -let hasAttachUnhandledExceptionListenersBeenCalled = false; - -type HandleErrorProps = { - error?: Error; - printMessagePreamble?: () => void; - message?: string; -}; - -/** - * Attaches process listeners to handle unhandled exceptions and rejections - */ -export const attachUnhandledExceptionListeners = (): void => { - if (hasAttachUnhandledExceptionListenersBeenCalled) { - return; - } - process.on('unhandledRejection', (reason) => { - process.exitCode = 1; - if (reason instanceof Error) { - void handleErrorSafe({ error: reason }); - } else if (typeof reason === 'string') { - // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors - void handleErrorSafe({ error: new Error(reason) }); - } else { - void handleErrorSafe({ - // eslint-disable-next-line amplify-backend-rules/prefer-amplify-errors - error: new Error(`Unhandled rejection of type [${typeof reason}]`, { - cause: reason, - }), - }); - } - }); - - process.on('uncaughtException', (error) => { - process.exitCode = 1; - void handleErrorSafe({ error }); - }); - hasAttachUnhandledExceptionListenersBeenCalled = true; -}; - -/** - * Generates a function that is intended to be used as a callback to yargs.fail() - * All logic for actually handling errors should be delegated to handleError. - */ -export const generateCommandFailureHandler = (): (( - message: string, - error: Error -) => Promise) => { - /** - * Format error output when a command fails - * @param message error message - * @param error error - */ - const handleCommandFailure = async (message: string, error?: Error) => { - await handleErrorSafe({ - error, - message, - }); - }; - return handleCommandFailure; -}; - -const handleErrorSafe = async (props: HandleErrorProps) => { - try { - await handleError(props); - } catch (e) { - printer.log(format.error(e), LogLevel.DEBUG); - // no-op should gracefully exit - return; - } -}; - -/** - * Error handling for uncaught errors during CLI command execution. - * - * This should be the one and only place where we handle unexpected errors. - * This includes console logging, debug logging, metrics recording, etc. - * (Note that we don't do all of those things yet, but this is where they should go) - */ -const handleError = async ({ - error, - printMessagePreamble, - message, -}: HandleErrorProps) => { - // If yargs threw an error because the customer force-closed a prompt (ie Ctrl+C during a prompt) then the intent to exit the process is clear - if (isUserForceClosePromptError(error)) { - return; - } - - printMessagePreamble?.(); - - if (error instanceof AmplifyError) { - printer.print(format.error(`${error.name}: ${error.message}`)); - - if (error.resolution) { - printer.print(`Resolution: ${error.resolution}`); - } - if (error.details) { - printer.print(`Details: ${error.details}`); - } - if (errorHasCauseMessage(error)) { - printer.print(`Cause: ${error.cause.message}`); - } - } else { - // non-Amplify Error object - printer.print(format.error(message || String(error))); - - if (errorHasCauseMessage(error)) { - printer.print(`Cause: ${error.cause.message}`); - } - } - - // additional debug logging for the stack traces - if (error?.stack) { - printer.log(error.stack, LogLevel.DEBUG); - } - if (errorHasCauseStackTrace(error)) { - printer.log(error.cause.stack, LogLevel.DEBUG); - } -}; - -const isUserForceClosePromptError = (err?: Error): boolean => { - return !!err && err?.message.includes('User force closed the prompt'); -}; - -const errorHasCauseStackTrace = ( - error?: Error -): error is Error & { cause: { stack: string } } => { - return ( - typeof error?.cause === 'object' && - !!error.cause && - 'stack' in error.cause && - typeof error.cause.stack === 'string' - ); -}; - -const errorHasCauseMessage = ( - error?: Error -): error is Error & { cause: { message: string } } => { - return ( - typeof error?.cause === 'object' && - !!error.cause && - 'message' in error.cause && - typeof error.cause.message === 'string' - ); -}; From 416d4eb81933311e22e4a740583a365f60b5a5ea Mon Sep 17 00:00:00 2001 From: PG-practice Date: Tue, 14 Jan 2025 16:03:37 +0000 Subject: [PATCH 4/4] Run 'npm run update:api' command to update API.mds --- packages/client-config/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-config/API.md b/packages/client-config/API.md index cccafacd46b..9d784bff525 100644 --- a/packages/client-config/API.md +++ b/packages/client-config/API.md @@ -627,7 +627,7 @@ export type CustomClientConfig = { export const DEFAULT_CLIENT_CONFIG_VERSION: ClientConfigVersion; // @public -export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ +export const generateClientConfig: (backendIdentifier: DeployedBackendIdentifier, version: T, awsClientProvider?: AWSClientProvider<{ getS3Client: S3Client; getAmplifyClient: AmplifyClient; getCloudFormationClient: CloudFormationClient;