From 44dad848fbed20e3b081dfa4cad6f41e00e05a0c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 23 Nov 2020 02:29:36 -0800 Subject: [PATCH] SAM debug: detect & surface "low disk space" #171 If `sam build` fills up the disk while building a Docker image (--use-container) it may fail with this message: RuntimeError: Container does not exist --- package.nls.json | 1 + src/shared/sam/cli/samCliBuild.ts | 28 +++++++++++++++++-- src/shared/sam/cli/samCliInvoker.ts | 26 +++++++++++------ src/shared/sam/cli/samCliInvokerUtils.ts | 7 +++-- src/shared/sam/localLambdaRunner.ts | 7 ++++- src/shared/utilities/childProcess.ts | 4 +-- .../shared/codelens/localLambdaRunner.test.ts | 1 + src/test/shared/sam/cli/samCliTestUtils.ts | 2 ++ .../sam/cli/testSamCliProcessInvoker.ts | 1 + 9 files changed, 61 insertions(+), 16 deletions(-) diff --git a/package.nls.json b/package.nls.json index bd2f9779075..82fcf6f990b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -362,6 +362,7 @@ "AWS.s3.validateBucketName.error.invalidStart": "Bucket name must start with a lowercase letter or number", "AWS.s3.validateBucketName.error.misusedPeriods": "Periods in bucket name must be surrounded by a lowercase letter or number", "AWS.s3.validateBucketName.error.resemblesIpAddress": "Bucket name must not resemble an IP address", + "AWS.sam.build.failure.diskSpace": "\"sam build\" failed. Check system disk space.", "AWS.sam.debugger.invalidRequest": "Debug Configuration has an unsupported request type. Supported types: {0}", "AWS.sam.debugger.invalidTarget": "Debug Configuration has an unsupported target type. Supported types: {0}", "AWS.sam.debugger.invalidRuntime": "AWS SAM debug: unknown runtime: {0}", diff --git a/src/shared/sam/cli/samCliBuild.ts b/src/shared/sam/cli/samCliBuild.ts index 12805feb150..b2634f1a9c6 100644 --- a/src/shared/sam/cli/samCliBuild.ts +++ b/src/shared/sam/cli/samCliBuild.ts @@ -9,7 +9,7 @@ import { logAndThrowIfUnexpectedExitCode, SamCliProcessInvoker } from './samCliI import { DefaultSamCliProcessInvoker } from './samCliInvoker' import { pushIf } from '../../utilities/collectionUtils' import { ext } from '../../extensionGlobals' -import { getChannelLogger } from '../../utilities/vsCodeUtils' +import { getChannelLogger, localize } from '../../utilities/vsCodeUtils' export interface SamCliBuildInvocationArguments { /** @@ -70,6 +70,8 @@ export interface FileFunctions { * An elaborate way to run `sam build`. */ export class SamCliBuildInvocation { + private _failure: string | undefined + public constructor( private readonly args: SamCliBuildInvocationArguments, private readonly context: { file: FileFunctions } = { file: getDefaultFileFunctions() } @@ -79,9 +81,15 @@ export class SamCliBuildInvocation { this.args.skipPullImage = !!this.args.skipPullImage } + /** Gets the failure message, or undefined if no failure was detected. */ + public failure(): string | undefined { + return this._failure + } + /** + * Invokes "sam build". * - * @returns process exit/status code + * @returns Process exit code, or -1 if `SamCliBuildInvocation` stopped the process and stored a failure message in `SamCliBuildInvocation.failure()`. */ public async execute(): Promise { await this.validate() @@ -113,14 +121,28 @@ export class SamCliBuildInvocation { ...this.args.environmentVariables, } + const checkFailure = (text: string): void => { + if (text.match(/(RuntimeError: Container does not exist)/)) { + this.args.invoker.stop() + this._failure = localize( + 'AWS.sam.build.failure.diskSpace', + '"sam build" failed. Check system disk space.' + ) + } + } + const childProcessResult = await this.args.invoker.invoke({ spawnOptions: { env }, arguments: invokeArgs, channelLogger: getChannelLogger(ext.outputChannel), + onStdout: checkFailure, + onStderr: checkFailure, }) + if (this._failure) { + return -1 + } logAndThrowIfUnexpectedExitCode(childProcessResult, 0) - return childProcessResult.exitCode } diff --git a/src/shared/sam/cli/samCliInvoker.ts b/src/shared/sam/cli/samCliInvoker.ts index c15f1f96325..f3cbf4f25f8 100644 --- a/src/shared/sam/cli/samCliInvoker.ts +++ b/src/shared/sam/cli/samCliInvoker.ts @@ -42,8 +42,16 @@ export function resolveSamCliProcessInvokerContext( * TODO: Merge this with `DefaultSamLocalInvokeCommand`. */ export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker { + private childProcess?: ChildProcess public constructor(private readonly context: SamCliProcessInvokerContext = resolveSamCliProcessInvokerContext()) {} + public stop(): void { + if (!this.childProcess) { + throw new Error('not started') + } + this.childProcess.stop() + } + public async invoke(options?: SamCliProcessInvokeOptions): Promise { const invokeOptions = makeRequiredSamCliProcessInvokeOptions(options) const logger = getLogger() @@ -56,22 +64,24 @@ export class DefaultSamCliProcessInvoker implements SamCliProcessInvoker { } const samCommand = sam.path ? sam.path : 'sam' - const childProcess: ChildProcess = new ChildProcess( - samCommand, - invokeOptions.spawnOptions, - ...invokeOptions.arguments - ) + this.childProcess = new ChildProcess(samCommand, invokeOptions.spawnOptions, ...invokeOptions.arguments) - options?.channelLogger?.info('AWS.running.command', 'Running command: {0}', `${childProcess}`) - logger.verbose(`running: ${childProcess}`) - return await childProcess.run( + options?.channelLogger?.info('AWS.running.command', 'Running command: {0}', `${this.childProcess}`) + logger.verbose(`running: ${this.childProcess}`) + return await this.childProcess.run( (text: string) => { options?.channelLogger?.emitMessage(text) logger.verbose(`stdout: ${text}`) + if (options?.onStdout) { + options.onStdout(text) + } }, (text: string) => { options?.channelLogger?.emitMessage(text) logger.verbose(`stderr: ${text}`) + if (options?.onStderr) { + options.onStderr(text) + } } ) } diff --git a/src/shared/sam/cli/samCliInvokerUtils.ts b/src/shared/sam/cli/samCliInvokerUtils.ts index a3a45c004e8..5a55caff254 100644 --- a/src/shared/sam/cli/samCliInvokerUtils.ts +++ b/src/shared/sam/cli/samCliInvokerUtils.ts @@ -5,7 +5,7 @@ import { SpawnOptions } from 'child_process' import { getLogger } from '../../logger' -import { ChildProcessResult } from '../../utilities/childProcess' +import { ChildProcessResult, ChildProcessStartArguments } from '../../utilities/childProcess' import { ChannelLogger } from '../../utilities/vsCodeUtils' export interface SamCliProcessInvokeOptions { @@ -13,11 +13,13 @@ export interface SamCliProcessInvokeOptions { arguments?: string[] /** Optionally log stdout and stderr to the specified logger */ channelLogger?: ChannelLogger + onStdout?: ChildProcessStartArguments['onStdout'] + onStderr?: ChildProcessStartArguments['onStderr'] } export function makeRequiredSamCliProcessInvokeOptions( options?: SamCliProcessInvokeOptions -): Required> { +): Required> { options = options || {} return { @@ -28,6 +30,7 @@ export function makeRequiredSamCliProcessInvokeOptions( export interface SamCliProcessInvoker { invoke(options?: SamCliProcessInvokeOptions): Promise + stop(): void } export function logAndThrowIfUnexpectedExitCode(processResult: ChildProcessResult, expectedExitCode: number): void { diff --git a/src/shared/sam/localLambdaRunner.ts b/src/shared/sam/localLambdaRunner.ts index 3c642ce3305..56c97608591 100644 --- a/src/shared/sam/localLambdaRunner.ts +++ b/src/shared/sam/localLambdaRunner.ts @@ -196,7 +196,12 @@ export async function invokeLambdaFunction( } try { - await new SamCliBuildInvocation(samCliArgs).execute() + const samBuild = new SamCliBuildInvocation(samCliArgs) + await samBuild.execute() + if (samBuild.failure()) { + ctx.chanLogger.emitMessage(samBuild.failure()!) + throw new Error(samBuild.failure()) + } } finally { // always delete temp template. await unlink(config.templatePath) diff --git a/src/shared/utilities/childProcess.ts b/src/shared/utilities/childProcess.ts index c916650c76d..4bd0ea13433 100644 --- a/src/shared/utilities/childProcess.ts +++ b/src/shared/utilities/childProcess.ts @@ -65,8 +65,8 @@ export class ChildProcess { * Calls `start()` with default listeners that resolve()/reject() on process end. */ public async run( - onStdout?: (text: string) => void, - onStderr?: (text: string) => void + onStdout?: ChildProcessStartArguments['onStdout'], + onStderr?: ChildProcessStartArguments['onStderr'] ): Promise { return await new Promise(async (resolve, reject) => { await this.start({ diff --git a/src/test/shared/codelens/localLambdaRunner.test.ts b/src/test/shared/codelens/localLambdaRunner.test.ts index 91977fc9f3e..3c83fef1254 100644 --- a/src/test/shared/codelens/localLambdaRunner.test.ts +++ b/src/test/shared/codelens/localLambdaRunner.test.ts @@ -291,6 +291,7 @@ describe('localLambdaRunner', async () => { templatePath: tempDir, manifestPath: undefined, // not needed for testing invoker: { + stop: () => {}, invoke: async (): Promise => isSuccessfulBuild ? successfulChildProcess : failedChildProcess, }, diff --git a/src/test/shared/sam/cli/samCliTestUtils.ts b/src/test/shared/sam/cli/samCliTestUtils.ts index 5b02522a5ff..b7a69a4ff20 100644 --- a/src/test/shared/sam/cli/samCliTestUtils.ts +++ b/src/test/shared/sam/cli/samCliTestUtils.ts @@ -14,6 +14,8 @@ import { ChildProcessResult } from '../../../../shared/utilities/childProcess' export class MockSamCliProcessInvoker implements SamCliProcessInvoker { public constructor(private readonly validateArgs: (args: string[]) => void) {} + public stop(): void {} + public async invoke(options?: SamCliProcessInvokeOptions): Promise { const invokeSettings = makeRequiredSamCliProcessInvokeOptions(options) diff --git a/src/test/shared/sam/cli/testSamCliProcessInvoker.ts b/src/test/shared/sam/cli/testSamCliProcessInvoker.ts index c7c3d42ae4c..39a6d0faa42 100644 --- a/src/test/shared/sam/cli/testSamCliProcessInvoker.ts +++ b/src/test/shared/sam/cli/testSamCliProcessInvoker.ts @@ -18,6 +18,7 @@ import { TestLogger } from '../../../testLogger' export class TestSamCliProcessInvoker implements SamCliProcessInvoker { public constructor(private readonly onInvoke: (spawnOptions: SpawnOptions, ...args: any[]) => ChildProcessResult) {} + public stop(): void {} public async invoke(options?: SamCliProcessInvokeOptions): Promise { const invokeOptions = makeRequiredSamCliProcessInvokeOptions(options)