Skip to content

Commit 7f87164

Browse files
committed
feat(lambda): allow debugging Lambda functions when connected to LocalStack
- Add LocalStack detection for Lambda debugging webview, by refactoring into two sub classes to handle the lifecycle of the debugging process. - Hide and disable version publishing - Await debugger start to slightly mitigate the race condition - Add LocalStack Lambda Debug Mode config deletion upon stopping debugging - Wait for function to be active before launching debugger - Add user-agent integration - Add temporary workaround to mitigate debugger attach timing issue - Fix debugger attach race condition by waiting using the new GET API - Add LDM error handling - Handle LocalStack Lambda hot reloading gracefully - Disable code download upon LocalStack hot reloading - Skip file watcher for hot-reloaded Lambda function - Disable Convert to SAM application for LocalStack - Show message in Remote Invoke WebView when connected to Localstack - Add telemetry for LocalStack case (including refactor some old telemetry)
1 parent 26dbb00 commit 7f87164

21 files changed

+892
-246
lines changed

packages/core/src/lambda/explorer/lambdaFunctionNode.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ import { LambdaFunctionFileNode } from './lambdaFunctionFileNode'
2121

2222
export const contextValueLambdaFunction = 'awsRegionFunctionNode'
2323
export const contextValueLambdaFunctionImportable = 'awsRegionFunctionNodeDownloadable'
24+
// Without "Convert to SAM application"
25+
export const contextValueLambdaFunctionDownloadOnly = 'awsRegionFunctionNodeDownloadableOnly'
26+
27+
function isLambdaFunctionDownloadable(contextValue?: string): boolean {
28+
return (
29+
contextValue === contextValueLambdaFunctionImportable || contextValue === contextValueLambdaFunctionDownloadOnly
30+
)
31+
}
2432

2533
export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNode {
2634
public constructor(
@@ -32,7 +40,7 @@ export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNo
3240
) {
3341
super(
3442
`${configuration.FunctionArn}`,
35-
contextValue === contextValueLambdaFunctionImportable
43+
isLambdaFunctionDownloadable(contextValue)
3644
? vscode.TreeItemCollapsibleState.Collapsed
3745
: vscode.TreeItemCollapsibleState.None
3846
)
@@ -72,7 +80,7 @@ export class LambdaFunctionNode extends AWSTreeNodeBase implements AWSResourceNo
7280
}
7381

7482
public override async getChildren(): Promise<AWSTreeNodeBase[]> {
75-
if (!(this.contextValue === contextValueLambdaFunctionImportable)) {
83+
if (!isLambdaFunctionDownloadable(this.contextValue)) {
7684
return []
7785
}
7886

packages/core/src/lambda/explorer/lambdaNodes.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
1414
import { PlaceholderNode } from '../../shared/treeview/nodes/placeholderNode'
1515
import { makeChildrenNodes } from '../../shared/treeview/utils'
1616
import { toArrayAsync, toMap, updateInPlace } from '../../shared/utilities/collectionUtils'
17-
import { listLambdaFunctions } from '../utils'
17+
import { listLambdaFunctions, isHotReloadingFunction } from '../utils'
1818
import {
1919
contextValueLambdaFunction,
2020
contextValueLambdaFunctionImportable,
21+
contextValueLambdaFunctionDownloadOnly,
2122
LambdaFunctionNode,
2223
} from './lambdaFunctionNode'
2324
import { samLambdaImportableRuntimes } from '../models/samLambdaRuntime'
25+
import { isLocalStackConnection } from '../../auth/utils'
2426

2527
/**
2628
* An AWS Explorer node representing the Lambda Service.
@@ -71,9 +73,15 @@ function makeLambdaFunctionNode(
7173
regionCode: string,
7274
configuration: Lambda.FunctionConfiguration
7375
): LambdaFunctionNode {
74-
const contextValue = samLambdaImportableRuntimes.contains(configuration.Runtime ?? '')
75-
? contextValueLambdaFunctionImportable
76-
: contextValueLambdaFunction
76+
let contextValue = contextValueLambdaFunction
77+
const isImportableRuntime = samLambdaImportableRuntimes.contains(configuration.Runtime ?? '')
78+
if (isLocalStackConnection()) {
79+
if (isImportableRuntime && !isHotReloadingFunction(configuration?.CodeSha256)) {
80+
contextValue = contextValueLambdaFunctionDownloadOnly
81+
}
82+
} else if (isImportableRuntime) {
83+
contextValue = contextValueLambdaFunctionImportable
84+
}
7785
const node = new LambdaFunctionNode(parent, regionCode, configuration, contextValue)
7886

7987
return node
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import globals from '../../shared/extensionGlobals'
8+
import type { Lambda } from 'aws-sdk'
9+
import { getLogger } from '../../shared/logger/logger'
10+
11+
const logger = getLogger()
12+
13+
export const remoteDebugSnapshotString = 'aws.lambda.remoteDebugSnapshot'
14+
15+
export interface DebugConfig {
16+
functionArn: string
17+
functionName: string
18+
port: number | undefined
19+
localRoot: string
20+
remoteRoot: string
21+
skipFiles: string[]
22+
shouldPublishVersion: boolean
23+
lambdaRuntime?: string // Lambda runtime (e.g., nodejs18.x)
24+
debuggerRuntime?: string // VS Code debugger runtime (e.g., node)
25+
outFiles?: string[]
26+
sourceMap?: boolean
27+
justMyCode?: boolean
28+
projectName?: string
29+
otherDebugParams?: string
30+
lambdaTimeout?: number
31+
layerArn?: string
32+
handlerFile?: string
33+
isLambdaRemote: boolean // false if LocalStack connection
34+
}
35+
36+
/**
37+
* Interface for debugging AWS Lambda functions remotely.
38+
*
39+
* This interface defines the contract for implementing remote debugging
40+
* for Lambda functions.
41+
*
42+
* Implementations of this interface handle the lifecycle of remote debugging sessions,
43+
* including checking health, set up, necessary deployment, and later clean up
44+
*/
45+
export interface LambdaDebugger {
46+
checkHealth(): Promise<void>
47+
setup(
48+
progress: vscode.Progress<{ message?: string; increment?: number }>,
49+
functionConfig: Lambda.FunctionConfiguration,
50+
region: string
51+
): Promise<void>
52+
waitForSetup(
53+
progress: vscode.Progress<{ message?: string; increment?: number }>,
54+
functionConfig: Lambda.FunctionConfiguration,
55+
region: string
56+
): Promise<void>
57+
waitForFunctionUpdates(progress: vscode.Progress<{ message?: string; increment?: number }>): Promise<void>
58+
cleanup(functionConfig: Lambda.FunctionConfiguration): Promise<void>
59+
}
60+
61+
// this should be called when the debug session is started
62+
export async function persistLambdaSnapshot(config: Lambda.FunctionConfiguration | undefined): Promise<void> {
63+
try {
64+
await globals.globalState.update(remoteDebugSnapshotString, config)
65+
} catch (error) {
66+
// TODO raise toolkit error
67+
logger.error(`Error persisting debug sessions: ${error}`)
68+
}
69+
}
70+
71+
export function getLambdaSnapshot(): Lambda.FunctionConfiguration | undefined {
72+
return globals.globalState.get<Lambda.FunctionConfiguration>(remoteDebugSnapshotString)
73+
}

packages/core/src/lambda/remoteDebugging/ldkClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function isTunnelInfo(data: TunnelInfo): data is TunnelInfo {
2626
)
2727
}
2828

29-
interface TunnelInfo {
29+
export interface TunnelInfo {
3030
tunnelID: string
3131
sourceToken: string
3232
destinationToken: string

0 commit comments

Comments
 (0)