Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@
"AWS.configuration.description.threatComposer.defaultEditor": "Use Threat Composer as the default editor for *.tc.json files.",
"AWS.command.accessanalyzer.iamPolicyChecks": "Open IAM Policy Checks",
"AWS.lambda.explorerTitle": "Explorer",
"AWS.lambda.local.invoke.progressTitle": "Local Invoke Function",
"AWS.lambda.remote.invoke.progressTitle": "Remote Invoke Function",
"AWS.lambda.invoke.succeeded.statusBarMessage": "$(testing-passed-icon) Invoke succeeded: {0}",
"AWS.lambda.invoke.completed.statusBarMessage": "$(testing-passed-icon) Invoke completed: {0}",
"AWS.lambda.invoke.failed.statusBarMessage": "$(testing-failed-icon) Invoke failed: {0}",
"AWS.developerTools.explorerTitle": "Developer Tools",
"AWS.codewhisperer.explorerTitle": "CodeWhisperer",
"AWS.appcomposer.explorerTitle": "Infrastructure Composer",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/lambda/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { LambdaFunctionNode } from './explorer/lambdaFunctionNode'
import { downloadLambdaCommand } from './commands/downloadLambda'
import { tryRemoveFolder } from '../shared/filesystemUtilities'
import { ExtContext } from '../shared/extensions'
import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda'
import { invokeRemoteLambda } from './vue/remoteInvoke/remoteInvokeBackend'
import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend'
import { Commands } from '../shared/vscode/commands2'
import { DefaultLambdaClient } from '../shared/clients/lambdaClient'
Expand Down
37 changes: 34 additions & 3 deletions packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,10 @@ export class SamInvokeWebview extends VueWebview {
}

/**
* Validate and execute the provided launch config.
* Validate and execute the provided launch config with a progress indicator in the VS Code window.
* TODO: Post validation failures back to webview?
* @param config Config to invoke
* @param source Optional source identifier
*/
public async invokeLaunchConfig(config: AwsSamDebuggerConfiguration, source?: string): Promise<void> {
const finalConfig = finalizeConfig(
Expand All @@ -357,9 +358,39 @@ export class SamInvokeWebview extends VueWebview {
const targetUri = await this.getUriFromLaunchConfig(finalConfig)
const folder = targetUri ? vscode.workspace.getWorkspaceFolder(targetUri) : undefined

// startDebugging on VS Code goes through the whole resolution chain
await vscode.debug.startDebugging(folder, finalConfig)
// Get function name to display in progress message
let functionName = 'Function'
if (config.invokeTarget.target === 'template' || config.invokeTarget.target === 'api') {
functionName = config.invokeTarget.logicalId
} else if (config.invokeTarget.target === 'code') {
functionName = config.name || config.invokeTarget.lambdaHandler
}

// Use withProgress in the extension host context
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Window,
title: localize('AWS.lambda.local.invoke.progressTitle', 'Local Invoke Function'),
cancellable: false,
Comment on lines +369 to +374
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode.debug.startDebugging already has its own debugger "stop" ui. why are we adding more ui on top of it?

Copy link
Contributor Author

@vicheey vicheey May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We recently allow invocation without debugging. So we should have this an option to stop long-running process. However, supporting cancelling the process required more effort and outside the scope of the current work. We will support it later.

},
async (progress) => {
// Start debugging
progress.report({
message: `${functionName}`,
})
return await vscode.debug.startDebugging(folder, finalConfig)
}
)
vscode.window.setStatusBarMessage(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status item disappears as soon as the invoke process complete. Use the vscode.window.setStatusBarMessage() to show final result and auto disappear after 5 second.

localize(
'AWS.lambda.invoke.completed.statusBarMessage',
'$(testing-passed-icon) Invoke completed: {0}',
functionName
),
5000
)
}

public async getLaunchConfigQuickPickItems(
launchConfig: LaunchConfiguration,
uri: vscode.Uri
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
class="button-theme-primary"
:style="{ width: '20%', marginRight: '27%' }"
v-on:click.prevent="launch"
:disabled="invokeInProgress"
>
Invoke
<span v-if="invokeInProgress">Invoking...</span>
<span v-else>Invoke</span>
</button>
<button class="button-theme-secondary" :style="{ marginLeft: '15px' }" v-on:click.prevent="loadConfig">
Load Debug Config
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/lambda/vue/configEditor/samInvokeFrontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface SamInvokeVueData {
showNameInput: boolean
newTestEventName: string
resourceData: ResourceData | undefined
invokeInProgress: boolean
}

function newLaunchConfig(existingConfig?: AwsSamDebuggerConfiguration): AwsSamDebuggerConfigurationLoose {
Expand Down Expand Up @@ -152,6 +153,7 @@ export default defineComponent({
selectedFile: '',
selectedFilePath: '',
resourceData: undefined,
invokeInProgress: false,
}
},
methods: {
Expand All @@ -168,11 +170,17 @@ export default defineComponent({
return // Exit early if config is not available
}

this.invokeInProgress = true
const source = this.resourceData?.source

client.invokeLaunchConfig(config, source).catch((e: Error) => {
try {
// Instead of using vscode.window.withProgress directly, delegate to the backend
await client.invokeLaunchConfig(config, source)
} catch (e: any) {
console.error(`invokeLaunchConfig failed: ${e.message}`)
})
} finally {
this.invokeInProgress = false
}
},
save() {
const config = this.formatConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
<div class="Icontainer">
<div><h1>Remote invoke configuration</h1></div>
<div class="form-row" style="justify-content: space-between; height: 28px">
<div><button class="button-theme-primary" v-on:click="sendInput">Remote Invoke</button></div>
<div>
<button class="button-theme-primary" v-on:click="sendInput" :disabled="invokeInProgress">
<span v-if="invokeInProgress">Invoking...</span>
<span v-else>Remote Invoke</span>
</button>
</div>
<div>
<span
:style="{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface RemoteInvokeData {
showNameInput: boolean
newTestEventName: string
selectedFunction: string
invokeInProgress: boolean
}
interface SampleQuickPickItem extends vscode.QuickPickItem {
filename: string
Expand All @@ -83,32 +84,59 @@ export class RemoteInvokeWebview extends VueWebview {
}

public async invokeLambda(input: string, source?: string): Promise<void> {
let result: Result = 'Succeeded'

this.channel.show()
this.channel.appendLine('Loading response...')

try {
const funcResponse = await this.client.invoke(this.data.FunctionArn, input)
const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : ''
const payload = funcResponse.Payload ? funcResponse.Payload : JSON.stringify({})

this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`)
this.channel.appendLine('Logs:')
this.channel.appendLine(logs)
this.channel.appendLine('')
this.channel.appendLine('Payload:')
this.channel.appendLine(String(payload))
this.channel.appendLine('')
} catch (e) {
const error = e as Error
this.channel.appendLine(`There was an error invoking ${this.data.FunctionArn}`)
this.channel.appendLine(error.toString())
this.channel.appendLine('')
result = 'Failed'
} finally {
telemetry.lambda_invokeRemote.emit({ result, passive: false, source: source })
}
await vscode.window.withProgress(
{
location: vscode.ProgressLocation.Window,
title: localize('AWS.lambda.remote.invoke.progressTitle', 'Remote Invoke Function'),
cancellable: false,
},
async (progress) => {
let result: Result = 'Succeeded'

progress.report({
message: `${this.data.FunctionName}`,
})

this.channel.show()
this.channel.appendLine('Loading response...')

try {
const funcResponse = await this.client.invoke(this.data.FunctionArn, input)
const logs = funcResponse.LogResult ? decodeBase64(funcResponse.LogResult) : ''
const payload = funcResponse.Payload ? funcResponse.Payload : JSON.stringify({})

this.channel.appendLine(`Invocation result for ${this.data.FunctionArn}`)
this.channel.appendLine('Logs:')
this.channel.appendLine(logs)
this.channel.appendLine('')
this.channel.appendLine('Payload:')
this.channel.appendLine(String(payload))
this.channel.appendLine('')
} catch (e) {
const error = e as Error
this.channel.appendLine(`There was an error invoking ${this.data.FunctionArn}`)
this.channel.appendLine(error.toString())
this.channel.appendLine('')
result = 'Failed'
} finally {
vscode.window.setStatusBarMessage(
result === 'Succeeded'
? localize(
'AWS.lambda.invoke.succeeded.statusBarMessage',
'$(testing-passed-icon) Invoke succeeded: {0}',
this.data.FunctionName
)
: localize(
'AWS.lambda.invoke.fail.statusBarMessage',
'$(testing-failed-icon) Invoke failed: {0}',
this.data.FunctionName
),
5000
)
telemetry.lambda_invokeRemote.emit({ result, passive: false, source: source })
}
}
)
}

public async promptFile() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { defineComponent } from 'vue'
import { WebviewClientFactory } from '../../../webviews/client'
import saveData from '../../../webviews/mixins/saveData'
import { RemoteInvokeData, RemoteInvokeWebview } from './invokeLambda'
import { RemoteInvokeData, RemoteInvokeWebview } from './remoteInvokeBackend'

const client = WebviewClientFactory.create<RemoteInvokeWebview>()
const defaultInitialData = {
Expand All @@ -21,6 +21,7 @@ const defaultInitialData = {
TestEvents: [],
FunctionStack: '',
Source: '',
invokeInProgress: false,
}

export default defineComponent({
Expand All @@ -46,6 +47,7 @@ export default defineComponent({
showNameInput: false,
newTestEventName: '',
selectedFunction: 'selectedFunction',
invokeInProgress: false,
}
},
methods: {
Expand Down Expand Up @@ -105,7 +107,7 @@ export default defineComponent({

async sendInput() {
let event = ''

this.invokeInProgress = true
if (this.payload === 'sampleEvents' || this.payload === 'savedEvents') {
event = this.sampleText
} else if (this.payload === 'localFile') {
Expand All @@ -117,6 +119,7 @@ export default defineComponent({
}
}
await client.invokeLambda(event, this.initialData.Source)
this.invokeInProgress = false
},

loadSampleEvent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@
*/

import assert from 'assert'
import { RemoteInvokeWebview, invokeRemoteLambda, InitialData } from '../../../../lambda/vue/remoteInvoke/invokeLambda'
import { LambdaClient, DefaultLambdaClient } from '../../../../shared/clients/lambdaClient'
import {
RemoteInvokeWebview,
invokeRemoteLambda,
InitialData,
} from '../../../lambda/vue/remoteInvoke/remoteInvokeBackend'
import { LambdaClient, DefaultLambdaClient } from '../../../shared/clients/lambdaClient'
import * as vscode from 'vscode'
import * as path from 'path'
import { makeTemporaryToolkitFolder } from '../../../../shared/filesystemUtilities'
import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
import sinon, { SinonStubbedInstance, createStubInstance } from 'sinon'
import { fs } from '../../../../shared'
import * as picker from '../../../../shared/ui/picker'
import { getTestWindow } from '../../../shared/vscode/window'
import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode'
import * as utils from '../../../../lambda/utils'
import { HttpResourceFetcher } from '../../../../shared/resourcefetcher/httpResourceFetcher'
import { ExtContext } from '../../../../shared/extensions'
import { FakeExtensionContext } from '../../../fakeExtensionContext'
import * as samCliRemoteTestEvent from '../../../../shared/sam/cli/samCliRemoteTestEvent'
import { TestEventsOperation, SamCliRemoteTestEventsParameters } from '../../../../shared/sam/cli/samCliRemoteTestEvent'
import { assertLogsContain } from '../../../globalSetup.test'
import { createResponse } from '../../../testUtil'
import { fs } from '../../../shared'
import * as picker from '../../../shared/ui/picker'
import { getTestWindow } from '../../shared/vscode/window'
import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode'
import * as utils from '../../../lambda/utils'
import { HttpResourceFetcher } from '../../../shared/resourcefetcher/httpResourceFetcher'
import { ExtContext } from '../../../shared/extensions'
import { FakeExtensionContext } from '../../fakeExtensionContext'
import * as samCliRemoteTestEvent from '../../../shared/sam/cli/samCliRemoteTestEvent'
import { TestEventsOperation, SamCliRemoteTestEventsParameters } from '../../../shared/sam/cli/samCliRemoteTestEvent'
import { assertLogsContain } from '../../globalSetup.test'
import { createResponse } from '../../testUtil'

describe('RemoteInvokeWebview', () => {
let outputChannel: vscode.OutputChannel
Expand Down Expand Up @@ -57,7 +61,7 @@ describe('RemoteInvokeWebview', () => {
})
})
describe('invokeLambda', () => {
it('invokes Lambda function successfully', async () => {
it('invokes Lambda function with payload', async () => {
const input = '{"key": "value"}'
const mockResponse = {
LogResult: Buffer.from('Test log').toString('base64'),
Expand All @@ -71,6 +75,13 @@ describe('RemoteInvokeWebview', () => {
}

await remoteInvokeWebview.invokeLambda(input)

const messages = getTestWindow().statusBar.messages
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages[0], 'Remote Invoke Function: testFunction')
assert.strictEqual(messages[1], '$(testing-passed-icon) Invoke succeeded: testFunction')

// Verify other assertions
assert(client.invoke.calledOnce)
assert(client.invoke.calledWith(data.FunctionArn, input))
assert.deepStrictEqual(appendedLines, [
Expand All @@ -84,6 +95,7 @@ describe('RemoteInvokeWebview', () => {
'',
])
})

it('handles Lambda invocation with no payload', async () => {
const mockResponse = {
LogResult: Buffer.from('Test log').toString('base64'),
Expand All @@ -98,6 +110,11 @@ describe('RemoteInvokeWebview', () => {

await remoteInvokeWebview.invokeLambda('')

const messages = getTestWindow().statusBar.messages
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages[0], 'Remote Invoke Function: testFunction')
assert.strictEqual(messages[1], '$(testing-passed-icon) Invoke succeeded: testFunction')

assert.deepStrictEqual(appendedLines, [
'Loading response...',
'Invocation result for arn:aws:lambda:us-west-2:123456789012:function:testFunction',
Expand All @@ -109,6 +126,7 @@ describe('RemoteInvokeWebview', () => {
'',
])
})

it('handles Lambda invocation with undefined LogResult', async () => {
const mockResponse = {
Payload: '{"result": "success"}',
Expand All @@ -122,6 +140,10 @@ describe('RemoteInvokeWebview', () => {
}

await remoteInvokeWebview.invokeLambda('{}')
const messages = getTestWindow().statusBar.messages
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages[0], 'Remote Invoke Function: testFunction')
assert.strictEqual(messages[1], '$(testing-passed-icon) Invoke succeeded: testFunction')

assert.deepStrictEqual(appendedLines, [
'Loading response...',
Expand All @@ -134,6 +156,7 @@ describe('RemoteInvokeWebview', () => {
'',
])
})

it('handles Lambda invocation error', async () => {
const input = '{"key": "value"}'
const mockError = new Error('Lambda invocation failed')
Expand All @@ -154,6 +177,10 @@ describe('RemoteInvokeWebview', () => {
err.message,
'telemetry: invalid Metric: "lambda_invokeRemote" emitted with result=Failed but without the `reason` property. Consider using `.run()` instead of `.emit()`, which will set these properties automatically. See https://github.com/aws/aws-toolkit-vscode/blob/master/docs/telemetry.md#guidelines'
)
const messages = getTestWindow().statusBar.messages
assert.strictEqual(messages.length, 2)
assert.strictEqual(messages[0], 'Remote Invoke Function: testFunction')
assert.strictEqual(messages[1], '$(testing-failed-icon) Invoke failed: testFunction')
}

assert.deepStrictEqual(appendedLines, [
Expand Down
Loading
Loading