diff --git a/src/commands/appSettings/connectionSettings/IConnectionPromptOptions.ts b/src/commands/appSettings/connectionSettings/IConnectionPromptOptions.ts index 0db3f971f..baae2bf8b 100644 --- a/src/commands/appSettings/connectionSettings/IConnectionPromptOptions.ts +++ b/src/commands/appSettings/connectionSettings/IConnectionPromptOptions.ts @@ -3,9 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues, type StorageConnectionTypeValues } from "../../../constants"; +import { type EventHubsConnectionType, type SqlDbConnectionType, type StorageConnectionType } from "./IConnectionTypesContext"; + export interface IConnectionPromptOptions { - preselectedConnectionType?: StorageConnectionTypeValues | EventHubsConnectionTypeValues | SqlDbConnectionTypeValues; + preselectedConnectionType?: StorageConnectionType | EventHubsConnectionType | SqlDbConnectionType; } diff --git a/src/commands/appSettings/connectionSettings/IConnectionTypesContext.ts b/src/commands/appSettings/connectionSettings/IConnectionTypesContext.ts new file mode 100644 index 000000000..9e26863af --- /dev/null +++ b/src/commands/appSettings/connectionSettings/IConnectionTypesContext.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ConnectionType } from "../../../constants"; + +export type StorageConnectionType = ConnectionType.Azure | ConnectionType.Emulator; +export type DTSConnectionType = ConnectionType; +export type EventHubsConnectionType = ConnectionType.Azure | ConnectionType.Emulator; +export type SqlDbConnectionType = ConnectionType.Azure | ConnectionType.Custom; + +export interface IConnectionTypesContext { + azureWebJobsStorageType?: StorageConnectionType; + dtsConnectionType?: DTSConnectionType; + eventHubsConnectionType?: EventHubsConnectionType; + sqlDbConnectionType?: SqlDbConnectionType; +} + diff --git a/src/commands/appSettings/connectionSettings/ISetConnectionSettingContext.ts b/src/commands/appSettings/connectionSettings/ISetConnectionSettingContext.ts index a318a622b..99b0127ea 100644 --- a/src/commands/appSettings/connectionSettings/ISetConnectionSettingContext.ts +++ b/src/commands/appSettings/connectionSettings/ISetConnectionSettingContext.ts @@ -5,8 +5,9 @@ import { type IActionContext } from "@microsoft/vscode-azext-utils"; import { type CodeActionValues, type ConnectionKey } from "../../../constants"; +import { type IConnectionTypesContext } from "./IConnectionTypesContext"; -export interface ISetConnectionSettingContext extends IActionContext { +export interface ISetConnectionSettingContext extends IActionContext, IConnectionTypesContext { action: CodeActionValues; projectPath: string; diff --git a/src/commands/appSettings/connectionSettings/azureWebJobsStorage/AzureWebJobsStoragePromptStep.ts b/src/commands/appSettings/connectionSettings/azureWebJobsStorage/AzureWebJobsStoragePromptStep.ts index 0752f3d3a..6c1b4a97a 100644 --- a/src/commands/appSettings/connectionSettings/azureWebJobsStorage/AzureWebJobsStoragePromptStep.ts +++ b/src/commands/appSettings/connectionSettings/azureWebJobsStorage/AzureWebJobsStoragePromptStep.ts @@ -6,11 +6,12 @@ import { StorageAccountKind, StorageAccountListStep, StorageAccountPerformance, StorageAccountReplication } from '@microsoft/vscode-azext-azureutils'; import { AzureWizardPromptStep, type ISubscriptionActionContext, type IWizardOptions } from '@microsoft/vscode-azext-utils'; import { type MessageItem } from 'vscode'; -import { ConnectionType, type EventHubsConnectionTypeValues, type SqlDbConnectionTypeValues } from '../../../../constants'; +import { ConnectionType } from '../../../../constants'; import { useEmulator } from '../../../../constants-nls'; import { ext } from '../../../../extensionVariables'; import { localize } from '../../../../localize'; import { type IConnectionPromptOptions } from '../IConnectionPromptOptions'; +import { type StorageConnectionType } from '../IConnectionTypesContext'; import { type IAzureWebJobsStorageWizardContext } from './IAzureWebJobsStorageWizardContext'; export class AzureWebJobsStoragePromptStep extends AzureWizardPromptStep { @@ -36,17 +37,16 @@ export class AzureWebJobsStoragePromptStep { + public async configureBeforePrompt(context: T): Promise { + const matchingConnectionType: StorageConnectionType | undefined = tryFindMatchingConnectionType([context.dtsConnectionType, context.eventHubsConnectionType, context.sqlDbConnectionType]); + if (this.options?.preselectedConnectionType === ConnectionType.Azure || this.options?.preselectedConnectionType === ConnectionType.Emulator) { context.azureWebJobsStorageType = this.options.preselectedConnectionType; } else if (!!context.storageAccount || !!context.newStorageAccountName) { // Only should prompt if no storage account was selected context.azureWebJobsStorageType = ConnectionType.Azure; - } else if (context.eventHubsConnectionType) { - context.azureWebJobsStorageType = context.eventHubsConnectionType; - } else if (context.sqlDbConnectionType === ConnectionType.Azure) { - // No official support for an `Emulator` scenario yet - context.azureWebJobsStorageType = context.sqlDbConnectionType; + } else if (matchingConnectionType) { + context.azureWebJobsStorageType = matchingConnectionType; } // Even if we end up skipping the prompt, we should still record the flow in telemetry @@ -88,3 +88,18 @@ export class AzureWebJobsStoragePromptStep = new Set([ConnectionType.Azure, ConnectionType.Emulator]); + +function tryFindMatchingConnectionType(connections: (ConnectionType | undefined)[]): StorageConnectionType | undefined { + for (const c of connections) { + if (!c) { + continue; + } + + if (availableStorageConnections.has(c)) { + return c as StorageConnectionType; + } + } + return undefined; +} diff --git a/src/commands/appSettings/connectionSettings/azureWebJobsStorage/IAzureWebJobsStorageWizardContext.ts b/src/commands/appSettings/connectionSettings/azureWebJobsStorage/IAzureWebJobsStorageWizardContext.ts index 3b05dab38..75f9ff73f 100644 --- a/src/commands/appSettings/connectionSettings/azureWebJobsStorage/IAzureWebJobsStorageWizardContext.ts +++ b/src/commands/appSettings/connectionSettings/azureWebJobsStorage/IAzureWebJobsStorageWizardContext.ts @@ -5,12 +5,12 @@ import { type StorageAccount } from "@azure/arm-storage"; import { type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; -import { type StorageConnectionTypeValues } from "../../../../constants"; +import { type StorageConnectionType } from "../IConnectionTypesContext"; import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext"; export interface IAzureWebJobsStorageWizardContext extends ISetConnectionSettingContext, Partial { storageAccount?: StorageAccount; newStorageAccountName?: string; - azureWebJobsStorageType?: StorageConnectionTypeValues; + azureWebJobsStorageType?: StorageConnectionType; } diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionCustomPromptStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionCustomPromptStep.ts new file mode 100644 index 000000000..a4982df79 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionCustomPromptStep.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, validationUtils } from '@microsoft/vscode-azext-utils'; +import { ConnectionType } from '../../../../constants'; +import { localize } from '../../../../localize'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSConnectionCustomPromptStep extends AzureWizardPromptStep { + public async prompt(context: T): Promise { + context.newDTSConnection = (await context.ui.showInputBox({ + prompt: localize('customDTSConnectionPrompt', 'Provide a custom DTS connection string.'), + validateInput: (value: string | undefined) => this.validateInput(value) + })).trim(); + } + + public shouldPrompt(context: T): boolean { + return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Custom; + } + + private validateInput(name: string | undefined): string | undefined { + name = name ? name.trim() : ''; + if (!validationUtils.hasValidCharLength(name)) { + return validationUtils.getInvalidCharLengthMessage(); + } + return undefined; + } +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionSetSettingStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionSetSettingStep.ts new file mode 100644 index 000000000..61889dcb1 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionSetSettingStep.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { nonNullProp } from '@microsoft/vscode-azext-utils'; +import { ConnectionKey } from '../../../../constants'; +import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSConnectionSetSettingStep extends SetConnectionSettingStepBase { + public priority: number = 240; + public debugDeploySetting: ConnectionKey = ConnectionKey.DTS; + + public async execute(context: T): Promise { + await this.setConnectionSetting(context, nonNullProp(context, 'newDTSConnection')); + } + + public shouldExecute(context: T): boolean { + return !!context.newDTSConnection; + } +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionTypeListStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionTypeListStep.ts new file mode 100644 index 000000000..4a4a1cbb6 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionTypeListStep.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, type AzureWizardExecuteStep, type IWizardOptions } from '@microsoft/vscode-azext-utils'; +import { type MessageItem } from 'vscode'; +import { ConnectionType } from '../../../../constants'; +import { useEmulator } from '../../../../constants-nls'; +import { localize } from '../../../../localize'; +import { DTSConnectionCustomPromptStep } from './DTSConnectionCustomPromptStep'; +import { DTSConnectionSetSettingStep } from './DTSConnectionSetSettingStep'; +import { DTSEmulatorStartStep } from './DTSEmulatorStartStep'; +import { DTSHubNameCustomPromptStep } from './DTSHubNameCustomPromptStep'; +import { DTSHubNameSetSettingStep } from './DTSHubNameSetSettingStep'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSConnectionTypeListStep extends AzureWizardPromptStep { + constructor(readonly connectionTypes: Set) { + super(); + } + + public async prompt(context: T): Promise { + const connectAzureButton = { title: localize('connectAzureTaskScheduler', 'Connect Azure Task Scheduler'), data: ConnectionType.Azure }; + const connectEmulatorButton = { title: useEmulator, data: ConnectionType.Emulator }; + const connectCustomDTSButton = { title: localize('connectCustomTaskScheduler', 'Connect Custom Task Scheduler'), data: ConnectionType.Custom }; + + const buttons: MessageItem[] = []; + if (this.connectionTypes.has(ConnectionType.Azure)) { + buttons.push(connectAzureButton); + } + if (this.connectionTypes.has(ConnectionType.Emulator)) { + buttons.push(connectEmulatorButton); + } + if (this.connectionTypes.has(ConnectionType.Custom)) { + buttons.push(connectCustomDTSButton); + } + + const message: string = localize('selectDTSConnection', 'In order to proceed, you must connect a Durable Task Scheduler for internal use by the Azure Functions runtime.'); + context.dtsConnectionType = (await context.ui.showWarningMessage(message, { modal: true }, ...buttons) as { + title: string; + data: ConnectionType; + }).data; + + context.telemetry.properties.dtsConnectionType = context.dtsConnectionType; + } + + public shouldPrompt(context: T): boolean { + return !context.dtsConnectionType; + } + + public async getSubWizard(context: T): Promise | undefined> { + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + switch (context.dtsConnectionType) { + case ConnectionType.Azure: + throw new Error('Needs implementation.'); + case ConnectionType.Emulator: + executeSteps.push(new DTSEmulatorStartStep()); + break; + case ConnectionType.Custom: + promptSteps.push( + new DTSConnectionCustomPromptStep(), + new DTSHubNameCustomPromptStep(), + ); + break; + } + + executeSteps.push( + new DTSConnectionSetSettingStep(), + new DTSHubNameSetSettingStep(), + ); + + return { promptSteps, executeSteps }; + } +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSEmulatorStartStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSEmulatorStartStep.ts new file mode 100644 index 000000000..a94a68600 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSEmulatorStartStep.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep, nonNullValue } from '@microsoft/vscode-azext-utils'; +import { commands } from 'vscode'; +import { ConnectionType } from '../../../../constants'; +import { localize } from '../../../../localize'; +import { type DurableTaskSchedulerEmulator } from '../../../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSEmulatorStartStep extends AzureWizardExecuteStep { + public priority: number = 200; + + public async execute(context: T): Promise { + const emulatorId: string = nonNullValue( + await commands.executeCommand('azureFunctions.durableTaskScheduler.startEmulator'), + localize('failedToStartEmulator', 'Internal error: Failed to start DTS emulator.'), + ); + + const emulators: DurableTaskSchedulerEmulator[] = nonNullValue( + await commands.executeCommand('azureFunctions.durableTaskScheduler.getEmulators'), + localize('failedToGetEmulators', 'Internal error: Failed to retrieve the list of DTS emulators.'), + ); + + const emulator: DurableTaskSchedulerEmulator = nonNullValue( + emulators.find(e => e.id === emulatorId), + localize('couldNotFindEmulator', 'Internal error: Failed to retrieve info on the started DTS emulator.'), + ); + + context.newDTSConnection = `Endpoint=${emulator.schedulerEndpoint};Authentication=None`; + context.newDTSHubName = 'default'; + } + + public shouldExecute(context: T): boolean { + return !context.newDTSConnection && context.dtsConnectionType === ConnectionType.Emulator; + } +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameCustomPromptStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameCustomPromptStep.ts new file mode 100644 index 000000000..376b892c8 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameCustomPromptStep.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, validationUtils, type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as path from 'path'; +import { ConnectionKey, ConnectionType, localSettingsFileName } from '../../../../constants'; +import { getLocalSettingsJson } from '../../../../funcConfig/local.settings'; +import { localize } from '../../../../localize'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSHubNameCustomPromptStep extends AzureWizardPromptStep { + public async prompt(context: T): Promise { + context.newDTSHubName = (await context.ui.showInputBox({ + prompt: localize('customDTSConnectionPrompt', 'Provide the custom DTS hub name.'), + value: await getDTSHubName(context, context.projectPath), + validateInput: (value: string) => this.validateInput(value) + })).trim(); + } + + public shouldPrompt(context: T): boolean { + return !context.newDTSHubName && context.dtsConnectionType === ConnectionType.Custom; + } + + private validateInput(name: string): string | undefined { + name = name.trim(); + + if (!validationUtils.hasValidCharLength(name)) { + return validationUtils.getInvalidCharLengthMessage(); + } + return undefined; + } +} + +async function getDTSHubName(context: IActionContext, projectPath: string): Promise { + const localSettingsJson = await getLocalSettingsJson(context, path.join(projectPath, localSettingsFileName)); + return localSettingsJson.Values?.[ConnectionKey.DTSHub]; +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameSetSettingStep.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameSetSettingStep.ts new file mode 100644 index 000000000..1a06ed03e --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/DTSHubNameSetSettingStep.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { nonNullProp } from '@microsoft/vscode-azext-utils'; +import { ConnectionKey } from '../../../../constants'; +import { SetConnectionSettingStepBase } from '../SetConnectionSettingStepBase'; +import { type IDTSConnectionWizardContext } from './IDTSConnectionWizardContext'; + +export class DTSHubNameSetSettingStep extends SetConnectionSettingStepBase { + public priority: number = 241; + public debugDeploySetting: ConnectionKey = ConnectionKey.DTSHub; + + public async execute(context: T): Promise { + await this.setConnectionSetting(context, nonNullProp(context, 'newDTSHubName')); + } + + public shouldExecute(context: T): boolean { + return !!context.newDTSHubName; + } +} diff --git a/src/commands/appSettings/connectionSettings/durableTaskScheduler/IDTSConnectionWizardContext.ts b/src/commands/appSettings/connectionSettings/durableTaskScheduler/IDTSConnectionWizardContext.ts new file mode 100644 index 000000000..47a6da469 --- /dev/null +++ b/src/commands/appSettings/connectionSettings/durableTaskScheduler/IDTSConnectionWizardContext.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ResourceGroup } from "@azure/arm-resources"; +import { type ConnectionType } from "../../../../constants"; +import { type StorageConnectionType } from "../IConnectionTypesContext"; +import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext"; + +export interface IDTSConnectionWizardContext extends ISetConnectionSettingContext { + resourceGroup?: ResourceGroup; + + // Connection Types + azureWebJobsStorageType?: StorageConnectionType; + dtsConnectionType?: ConnectionType; + + newDTSConnection?: string; + newDTSHubName?: string; +} diff --git a/src/commands/appSettings/connectionSettings/eventHubs/EventHubsConnectionPromptStep.ts b/src/commands/appSettings/connectionSettings/eventHubs/EventHubsConnectionPromptStep.ts index 6ef8e9518..ef929d3e4 100644 --- a/src/commands/appSettings/connectionSettings/eventHubs/EventHubsConnectionPromptStep.ts +++ b/src/commands/appSettings/connectionSettings/eventHubs/EventHubsConnectionPromptStep.ts @@ -39,7 +39,7 @@ export class EventHubsConnectionPromptStep { @@ -51,7 +51,7 @@ export class EventHubsConnectionPromptStep { resourceGroup?: ResourceGroup; // Connection Types - azureWebJobsStorageType?: StorageConnectionTypeValues; - sqlDbConnectionType?: SqlDbConnectionTypeValues; + azureWebJobsStorageType?: StorageConnectionType; + sqlDbConnectionType?: SqlDbConnectionType; // SQL newSqlServerName?: string; diff --git a/src/commands/durableTaskScheduler/startEmulator.ts b/src/commands/durableTaskScheduler/startEmulator.ts index 31fb87bb9..e1528ea3d 100644 --- a/src/commands/durableTaskScheduler/startEmulator.ts +++ b/src/commands/durableTaskScheduler/startEmulator.ts @@ -7,7 +7,7 @@ import { type IActionContext } from "@microsoft/vscode-azext-utils"; import { type DurableTaskSchedulerEmulatorClient } from "../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient"; export function startEmulatorCommandFactory(emulatorClient: DurableTaskSchedulerEmulatorClient) { - return async (_: IActionContext) => { - await emulatorClient.startEmulator(); + return async (_: IActionContext): Promise => { + return await emulatorClient.startEmulator(); }; } diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index d68c9c0b2..1c0839e8b 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -21,6 +21,7 @@ import { installOrUpdateFuncCoreTools } from '../funcCoreTools/installOrUpdateFu import { uninstallFuncCoreTools } from '../funcCoreTools/uninstallFuncCoreTools'; import { type DurableTaskSchedulerClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerClient'; import { type DurableTaskSchedulerDataBranchProvider } from '../tree/durableTaskScheduler/DurableTaskSchedulerDataBranchProvider'; +import { type DurableTaskSchedulerEmulator, type DurableTaskSchedulerEmulatorClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; import { ResolvedFunctionAppResource } from '../tree/ResolvedFunctionAppResource'; import { addBinding } from './addBinding/addBinding'; import { addLocalMIConnections } from './addMIConnections/addLocalMIConnections'; @@ -50,6 +51,7 @@ import { disconnectRepo } from './deployments/disconnectRepo'; import { redeployDeployment } from './deployments/redeployDeployment'; import { viewCommitInGitHub } from './deployments/viewCommitInGitHub'; import { viewDeploymentLogs } from './deployments/viewDeploymentLogs'; +import { copyEmulatorConnectionStringCommandFactory } from './durableTaskScheduler/copyEmulatorConnectionString'; import { copySchedulerConnectionStringCommandFactory } from './durableTaskScheduler/copySchedulerConnectionString'; import { copySchedulerEndpointCommandFactory } from './durableTaskScheduler/copySchedulerEndpoint'; import { createSchedulerCommandFactory } from './durableTaskScheduler/createScheduler'; @@ -57,6 +59,8 @@ import { createTaskHubCommandFactory } from './durableTaskScheduler/createTaskHu import { deleteSchedulerCommandFactory } from './durableTaskScheduler/deleteScheduler'; import { deleteTaskHubCommandFactory } from './durableTaskScheduler/deleteTaskHub'; import { openTaskHubDashboard } from './durableTaskScheduler/openTaskHubDashboard'; +import { startEmulatorCommandFactory } from './durableTaskScheduler/startEmulator'; +import { stopEmulatorCommandFactory } from './durableTaskScheduler/stopEmulator'; import { editAppSetting } from './editAppSetting'; import { EventGridCodeLensProvider } from './executeFunction/eventGrid/EventGridCodeLensProvider'; import { sendEventGridRequest } from './executeFunction/eventGrid/sendEventGridRequest'; @@ -79,10 +83,6 @@ import { stopFunctionApp } from './stopFunctionApp'; import { swapSlot } from './swapSlot'; import { disableFunction, enableFunction } from './updateDisabledState'; import { viewProperties } from './viewProperties'; -import { startEmulatorCommandFactory } from './durableTaskScheduler/startEmulator'; -import { stopEmulatorCommandFactory } from './durableTaskScheduler/stopEmulator'; -import { type DurableTaskSchedulerEmulatorClient } from '../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient'; -import { copyEmulatorConnectionStringCommandFactory } from './durableTaskScheduler/copyEmulatorConnectionString'; export function registerCommands( services: { @@ -204,4 +204,5 @@ export function registerCommands( registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.openTaskHubDashboard', openTaskHubDashboard); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.startEmulator', startEmulatorCommandFactory(services.dts.emulatorClient)); registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.stopEmulator', stopEmulatorCommandFactory(services.dts.emulatorClient)); + registerCommandWithTreeNodeUnwrapping('azureFunctions.durableTaskScheduler.getEmulators', async (_: IActionContext): Promise => await services.dts.emulatorClient.getEmulators()); } diff --git a/src/constants.ts b/src/constants.ts index 319a714f1..d168a72dc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -126,11 +126,6 @@ export enum DurableBackend { SQL = 'mssql', } -export type ConnectionTypeValues = typeof ConnectionType[keyof typeof ConnectionType]; -export type StorageConnectionTypeValues = Exclude; -export type EventHubsConnectionTypeValues = Exclude; -export type SqlDbConnectionTypeValues = Exclude; - export type CodeActionValues = typeof CodeAction[keyof typeof CodeAction]; export type ConnectionKeyValues = typeof ConnectionKey[keyof typeof ConnectionKey]; export type DurableBackendValues = typeof DurableBackend[keyof typeof DurableBackend]; diff --git a/src/debug/durable/validateDTSConnectionPreDebug.ts b/src/debug/durable/validateDTSConnectionPreDebug.ts new file mode 100644 index 000000000..7047ea27b --- /dev/null +++ b/src/debug/durable/validateDTSConnectionPreDebug.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizard, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { DTSConnectionTypeListStep } from "../../commands/appSettings/connectionSettings/durableTaskScheduler/DTSConnectionTypeListStep"; +import { type IDTSConnectionWizardContext } from "../../commands/appSettings/connectionSettings/durableTaskScheduler/IDTSConnectionWizardContext"; +import { CodeAction, ConnectionKey, ConnectionType } from "../../constants"; +import { getLocalSettingsConnectionString } from "../../funcConfig/local.settings"; +import { localize } from "../../localize"; +import { requestUtils } from "../../utils/requestUtils"; + +// If the user previously chose to debug using the emulator, leverage that preference for the remaining VS Code session +let useDTSEmulator: boolean; + +export async function validateDTSConnectionPreDebug(context: IActionContext, projectPath: string): Promise { + const dtsConnection: string | undefined = await getLocalSettingsConnectionString(context, ConnectionKey.DTS, projectPath); + if (dtsConnection && await isAliveConnection(context, dtsConnection)) { + return; + } + + const availableDebugConnectionTypes = new Set([ConnectionType.Emulator, ConnectionType.Custom]); + + const wizardContext: IDTSConnectionWizardContext = Object.assign(context, { + projectPath, + action: CodeAction.Debug, + dtsConnectionType: useDTSEmulator ? ConnectionType.Emulator : undefined, + }); + + const wizard: AzureWizard = new AzureWizard(wizardContext, { + title: localize('prepareDTSDebugConnection', 'Prepare DTS debug connection'), + promptSteps: [new DTSConnectionTypeListStep(availableDebugConnectionTypes)], + }); + + await wizard.prompt(); + await wizard.execute(); + + useDTSEmulator = wizardContext.dtsConnectionType === ConnectionType.Emulator; +} + +/** + * Checks whether a given DTS connection is still alive (i.e. has not gone stale) + */ +export async function isAliveConnection(context: IActionContext, dtsConnection: string): Promise { + // We need to extract the endpoint from a string like: Endpoint=http://localhost:55053/;Authentication=None + const endpointMatch = dtsConnection.match(/Endpoint=([^;]+)/); + if (!endpointMatch) { + return false; + } + + try { + const url: string = endpointMatch[1]; + await requestUtils.sendRequestWithExtTimeout(context, { url, method: 'GET' }); + return true; + } catch (e) { + // Even if we get back an error, if we can read a status code, the connection provided a response and is still alive + const statusCode = (e as { statusCode?: number })?.statusCode; + return statusCode ? true : false; + } +} diff --git a/src/debug/validatePreDebug.ts b/src/debug/validatePreDebug.ts index 381fd61f9..5cdfa095e 100644 --- a/src/debug/validatePreDebug.ts +++ b/src/debug/validatePreDebug.ts @@ -13,7 +13,7 @@ import { validateStorageConnection } from '../commands/appSettings/connectionSet import { validateEventHubsConnection } from '../commands/appSettings/connectionSettings/eventHubs/validateEventHubsConnection'; import { validateSqlDbConnection } from '../commands/appSettings/connectionSettings/sqlDatabase/validateSqlDbConnection'; import { tryGetFunctionProjectRoot } from '../commands/createNewProject/verifyIsProject'; -import { CodeAction, ConnectionKey, DurableBackend, ProjectLanguage, functionJsonFileName, localSettingsFileName, localStorageEmulatorConnectionString, projectLanguageModelSetting, projectLanguageSetting, workerRuntimeKey, type DurableBackendValues } from "../constants"; +import { CodeAction, ConnectionKey, DurableBackend, ProjectLanguage, functionJsonFileName, localSettingsFileName, localStorageEmulatorConnectionString, projectLanguageModelSetting, projectLanguageSetting, workerRuntimeKey } from "../constants"; import { ParsedFunctionJson } from "../funcConfig/function"; import { MismatchBehavior, getLocalSettingsConnectionString, setLocalAppSetting } from "../funcConfig/local.settings"; import { getLocalFuncCoreToolsVersion } from '../funcCoreTools/getLocalFuncCoreToolsVersion'; @@ -24,6 +24,7 @@ import { durableUtils } from '../utils/durableUtils'; import { isNodeV4Plus, isPythonV2Plus } from '../utils/programmingModelUtils'; import { getDebugConfigs, isDebugConfigEqual } from '../vsCodeConfig/launch'; import { getWorkspaceSetting, tryGetFunctionsWorkerRuntimeForProject } from "../vsCodeConfig/settings"; +import { validateDTSConnectionPreDebug } from './durable/validateDTSConnectionPreDebug'; export interface IPreDebugValidateResult { workspace: vscode.WorkspaceFolder; @@ -52,10 +53,11 @@ export async function preDebugValidate(actionContext: IActionContext, debugConfi if (context.projectPath) { const projectLanguage: string | undefined = getWorkspaceSetting(projectLanguageSetting, context.projectPath); const projectLanguageModel: number | undefined = getWorkspaceSetting(projectLanguageModelSetting, context.projectPath); - const durableStorageType: DurableBackendValues | undefined = await durableUtils.getStorageTypeFromWorkspace(projectLanguage, context.projectPath); + const durableStorageType: DurableBackend | undefined = await durableUtils.getStorageTypeFromWorkspace(projectLanguage, context.projectPath); context.telemetry.properties.projectLanguage = projectLanguage; context.telemetry.properties.projectLanguageModel = projectLanguageModel?.toString(); + context.telemetry.properties.durableStorageType = durableStorageType; context.telemetry.properties.lastValidateStep = 'functionVersion'; shouldContinue = await validateFunctionVersion(context, projectLanguage, projectLanguageModel, workspace.uri.fsPath); @@ -64,6 +66,10 @@ export async function preDebugValidate(actionContext: IActionContext, debugConfi await validateWorkerRuntime(context, projectLanguage, context.projectPath); switch (durableStorageType) { + case DurableBackend.DTS: + context.telemetry.properties.lastValidateStep = 'dtsConnection'; + await validateDTSConnectionPreDebug(context, context.projectPath); + break; case DurableBackend.Netherite: context.telemetry.properties.lastValidateStep = 'eventHubsConnection'; await validateEventHubsConnection(context, context.projectPath);