Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(lambda): fix sync integration test #5948

Merged
merged 9 commits into from
Nov 11, 2024
5 changes: 3 additions & 2 deletions packages/core/src/shared/sam/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from 'vscode'
import { TemplateItem, createTemplatePrompter, getSamCliPathAndVersion, runInTerminal } from './sync'
import { TemplateItem, createTemplatePrompter } from './sync'
import { ChildProcess } from '../utilities/processUtils'
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
import { Wizard } from '../wizards/wizard'
Expand All @@ -19,8 +19,9 @@ import globals from '../extensionGlobals'
import { TreeNode } from '../treeview/resourceTreeDataProvider'
import { telemetry } from '../telemetry/telemetry'
import { getSpawnEnv } from '../env/resolveEnv'
import { getProjectRoot, isDotnetRuntime } from './utils'
import { getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils'
import { getConfigFileUri, validateSamBuildConfig } from './config'
import { runInTerminal } from './processTerminal'

export interface BuildParams {
readonly template: TemplateItem
Expand Down
12 changes: 3 additions & 9 deletions packages/core/src/shared/sam/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,9 @@ import { CancellationError } from '../utilities/timeoutUtils'
import { Wizard } from '../wizards/wizard'
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
import { validateSamDeployConfig, SamConfig, writeSamconfigGlobal } from './config'
import {
TemplateItem,
createStackPrompter,
createBucketPrompter,
createTemplatePrompter,
getSamCliPathAndVersion,
runInTerminal,
} from './sync'
import { getProjectRoot, getSource } from './utils'
import { TemplateItem, createStackPrompter, createBucketPrompter, createTemplatePrompter } from './sync'
import { getProjectRoot, getSamCliPathAndVersion, getSource } from './utils'
import { runInTerminal } from './processTerminal'

export interface DeployParams {
readonly paramsSource: ParamsSource
Expand Down
146 changes: 146 additions & 0 deletions packages/core/src/shared/sam/processTerminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'

import { ToolkitError, UnknownError } from '../errors'
import globals from '../extensionGlobals'
import { isCloud9 } from '../extensionUtilities'
import { ChildProcess, ChildProcessResult } from '../utilities/processUtils'
import { CancellationError } from '../utilities/timeoutUtils'
import { getLogger } from '../logger'
import { removeAnsi } from '../utilities/textUtilities'
import { isAutomation } from '../vscode/env'

let oldTerminal: ProcessTerminal | undefined
export async function runInTerminal(proc: ChildProcess, cmd: string) {
const handleResult = (result?: ChildProcessResult) => {
if (result && result.exitCode !== 0) {
const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}`
if (result.stderr.includes('is up to date')) {
throw ToolkitError.chain(result.error, message, {
code: 'NoUpdateExitCode',
})
}
throw ToolkitError.chain(result.error, message, {
code: 'NonZeroExitCode',
})
}
}

// `createTerminal` doesn't work on C9 so we use the output channel instead
if (isCloud9()) {
globals.outputChannel.show()

const result = proc.run({
onStdout: (text) => globals.outputChannel.append(removeAnsi(text)),
onStderr: (text) => globals.outputChannel.append(removeAnsi(text)),
})
await proc.send('\n')

return handleResult(await result)
}

// The most recent terminal won't get garbage collected until the next run
if (oldTerminal?.stopped === true) {
oldTerminal.close()
}
const pty = (oldTerminal = new ProcessTerminal(proc))
const terminal = vscode.window.createTerminal({ pty, name: `SAM ${cmd}` })
terminal.sendText('\n')
terminal.show()

const result = await new Promise<ChildProcessResult>((resolve) => pty.onDidExit(resolve))
if (pty.cancelled) {
throw result.error !== undefined
? ToolkitError.chain(result.error, 'SAM CLI was cancelled before exiting', { cancelled: true })
: new CancellationError('user')
} else {
return handleResult(result)
}
}

// This is a decent improvement over using the output channel but it isn't a tty/pty
// SAM CLI uses `click` which has reduced functionality if `os.isatty` returns false
// Historically, Windows lack of a pty-equivalent is why it's not available in libuv
// Maybe it's doable now with the ConPTY API? https://github.com/libuv/libuv/issues/2640
class ProcessTerminal implements vscode.Pseudoterminal {
private readonly onDidCloseEmitter = new vscode.EventEmitter<number | void>()
private readonly onDidWriteEmitter = new vscode.EventEmitter<string>()
private readonly onDidExitEmitter = new vscode.EventEmitter<ChildProcessResult>()
public readonly onDidWrite = this.onDidWriteEmitter.event
public readonly onDidClose = this.onDidCloseEmitter.event
public readonly onDidExit = this.onDidExitEmitter.event

public constructor(private readonly process: ChildProcess) {
// Used in integration tests
if (isAutomation()) {
// Disable because it is a test.
// eslint-disable-next-line aws-toolkits/no-console-log
this.onDidWrite((text) => console.log(text.trim()))
}
}

#cancelled = false
public get cancelled() {
return this.#cancelled
}

public get stopped() {
return this.process.stopped
}

public open(initialDimensions: vscode.TerminalDimensions | undefined): void {
this.process
.run({
onStdout: (text) => this.mapStdio(text),
onStderr: (text) => this.mapStdio(text),
})
.then((result) => this.onDidExitEmitter.fire(result))
.catch((err) =>
this.onDidExitEmitter.fire({ error: UnknownError.cast(err), exitCode: -1, stderr: '', stdout: '' })
)
.finally(() => this.onDidWriteEmitter.fire('\r\nPress any key to close this terminal'))
}

public close(): void {
this.process.stop()
this.onDidCloseEmitter.fire()
}

public handleInput(data: string) {
// ETX
if (data === '\u0003' || this.process.stopped) {
this.#cancelled ||= data === '\u0003'
return this.close()
}

// enter
if (data === '\u000D') {
this.process.send('\n').then(undefined, (e) => {
getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message)
})
this.onDidWriteEmitter.fire('\r\n')
} else {
this.process.send(data).then(undefined, (e) => {
getLogger().error('ProcessTerminal: process.send() failed: %s', (e as Error).message)
})
this.onDidWriteEmitter.fire(data)
}
}

private mapStdio(text: string): void {
const lines = text.split('\n')
const first = lines.shift()

if (first) {
this.onDidWriteEmitter.fire(first)
}

for (const line of lines) {
this.onDidWriteEmitter.fire('\r\n')
this.onDidWriteEmitter.fire(line)
}
}
}
Loading
Loading