Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import {
assetsFolderName,
autoRuntimeDependenciesPathSettingKey,
devContainerFolderName,
builtinOperationSdksFolderName,
extensionCommand,
funcIgnoreFileName,
gitignoreFileName,
hostFileName,
jarFolderName,
libDirectory,
localSettingsFileName,
lspDirectory,
Expand Down Expand Up @@ -55,8 +57,8 @@ export async function createRulesFiles(context: IFunctionWizardContext): Promise
}

export async function createLibFolder(context: IFunctionWizardContext): Promise<void> {
fse.mkdirSync(path.join(context.projectPath, libDirectory, 'builtinOperationSdks', 'JAR'), { recursive: true });
fse.mkdirSync(path.join(context.projectPath, libDirectory, 'builtinOperationSdks', 'net472'), { recursive: true });
fse.mkdirSync(path.join(context.projectPath, libDirectory, builtinOperationSdksFolderName, jarFolderName), { recursive: true });
fse.mkdirSync(path.join(context.projectPath, libDirectory, builtinOperationSdksFolderName, 'net472'), { recursive: true });
}

export async function createLogicAppAndWorkflow(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import * as localSettings from '../../utils/appSettings/localSettings';
import { writeFormattedJson } from '../../utils/fs';
import { hasCodefulSdkReference, hasCodefulWorkflowSetting } from '../../utils/codeful';
import { isCustomCodeFunctionsProjectInRoot, tryGetLogicAppCustomCodeFunctionsProjects } from '../../utils/customCodeUtils';
import { hasJdbcDriverJars } from '../../utils/java/jdbcConnector';
import { isManagedIdentityAuthEnabled, useNodeDesignTimeWorker } from '../../utils/vsCodeConfig/settings';
import {
extractAppSettingReferences,
Expand Down Expand Up @@ -72,6 +73,16 @@ vi.mock('../../utils/customCodeUtils', () => ({
tryGetLogicAppCustomCodeFunctionsProjects: vi.fn(() => Promise.resolve(undefined)),
}));

// Keep the real (pure) mergeMultiLanguageWorkerFlag but stub JAR detection so tests control whether the
// project has JDBC driver JARs without depending on the shared fse.readdir mock's path behavior.
vi.mock('../../utils/java/jdbcConnector', async (importActual) => {
const actual = await importActual<typeof import('../../utils/java/jdbcConnector')>();
return {
...actual,
hasJdbcDriverJars: vi.fn(() => Promise.resolve(false)),
};
});

vi.mock('../../utils/vsCodeConfig/settings', async (importActual) => {
const actual = await importActual<typeof import('../../utils/vsCodeConfig/settings')>();
return {
Expand All @@ -98,6 +109,7 @@ const mockedIsCodeful = hasCodefulSdkReference as unknown as ReturnType<typeof v
const mockedIsCustomCodeInRoot = isCustomCodeFunctionsProjectInRoot as unknown as ReturnType<typeof vi.fn>;
const mockedHasCodefulWorkflowSetting = hasCodefulWorkflowSetting as unknown as ReturnType<typeof vi.fn>;
const mockedTryGetCustomCodeProjects = tryGetLogicAppCustomCodeFunctionsProjects as unknown as ReturnType<typeof vi.fn>;
const mockedHasJdbcJars = hasJdbcDriverJars as unknown as ReturnType<typeof vi.fn>;
const mockedAppendLog = ext.outputChannel.appendLog as unknown as ReturnType<typeof vi.fn>;

/** Returns every line written to the output channel via appendLog. */
Expand Down Expand Up @@ -129,6 +141,7 @@ describe('projectFilesConsistency', () => {
vi.clearAllMocks();
mockedIsCodeful.mockResolvedValue(false);
mockedIsCustomCodeInRoot.mockResolvedValue(false);
mockedHasJdbcJars.mockResolvedValue(false);
mockedFse.readdir.mockResolvedValue([]);
vi.mocked(useNodeDesignTimeWorker).mockReturnValue(false);
(workspace as any).fs = { createDirectory: vi.fn(() => Promise.resolve()) };
Expand Down Expand Up @@ -261,6 +274,73 @@ describe('projectFilesConsistency', () => {
});
});

// Self-heal for issue #8597: the JDBC built-in connector needs the Functions multi-language (Java)
// worker, which is only enabled by AzureWebJobsFeatureFlags=EnableMultiLanguageWorker. A plain codeless
// logic app does not get that flag, so when the user drops driver JAR(s) into lib/builtinOperationSdks/JAR
// regeneration adds/merges the flag automatically.
describe('ensureLocalSettingsFile — JDBC multi-language worker self-heal', () => {
const fullCodelessValues = {
APP_KIND: 'workflowapp',
FUNCTIONS_WORKER_RUNTIME: 'dotnet',
ProjectDirectoryPath: projectPath,
AzureWebJobsStorage: 'UseDevelopmentStorage=true',
FUNCTIONS_INPROC_NET8_ENABLED: '1',
};

beforeEach(() => {
mockFiles({ [`${projectPath}/local.settings.json`]: '{}' });
mockedFse.readdir.mockResolvedValue([]);
});

it('adds AzureWebJobsFeatureFlags=EnableMultiLanguageWorker for a codeless logic app when JDBC JARs are present', async () => {
mockedHasJdbcJars.mockResolvedValue(true);
mockedGetLocalSettingsJson.mockResolvedValue({ IsEncrypted: false, Values: { ...fullCodelessValues } });

const { changed } = await ensureLocalSettingsFile(context, projectPath);

expect(changed).toBe(true);
const settingsAdded = mockedAddOrUpdate.mock.calls[0][2];
expect(settingsAdded).toEqual({ [azureWebJobsFeatureFlagsKey]: multiLanguageWorkerSetting });
});

it('merges the flag with existing AzureWebJobsFeatureFlags without clobbering other flags', async () => {
mockedHasJdbcJars.mockResolvedValue(true);
mockedGetLocalSettingsJson.mockResolvedValue({
IsEncrypted: false,
Values: { ...fullCodelessValues, [azureWebJobsFeatureFlagsKey]: 'SomeOtherFlag' },
});

const { changed } = await ensureLocalSettingsFile(context, projectPath);

expect(changed).toBe(true);
const settingsAdded = mockedAddOrUpdate.mock.calls[0][2];
expect(settingsAdded).toEqual({ [azureWebJobsFeatureFlagsKey]: `SomeOtherFlag,${multiLanguageWorkerSetting}` });
});

it('does not change anything when the flag is already present and nothing else is missing', async () => {
mockedHasJdbcJars.mockResolvedValue(true);
mockedGetLocalSettingsJson.mockResolvedValue({
IsEncrypted: false,
Values: { ...fullCodelessValues, [azureWebJobsFeatureFlagsKey]: multiLanguageWorkerSetting },
});

const { changed } = await ensureLocalSettingsFile(context, projectPath);

expect(changed).toBe(false);
expect(mockedAddOrUpdate).not.toHaveBeenCalled();
});

it('does not add the flag for a codeless logic app when no JDBC JARs are present', async () => {
mockedHasJdbcJars.mockResolvedValue(false);
mockedGetLocalSettingsJson.mockResolvedValue({ IsEncrypted: false, Values: { ...fullCodelessValues } });

const { changed } = await ensureLocalSettingsFile(context, projectPath);

expect(changed).toBe(false);
expect(mockedAddOrUpdate).not.toHaveBeenCalled();
});
});

// Behavior by logic app type: regeneration builds the root local.settings.json from the same shared
// source of truth as fresh project creation (getLocalSettingsSchema). The project type is inferred from
// the project files (detectProjectType): codeful via hasCodefulWorkflowSetting/hasCodefulSdkReference,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,16 @@ describe('generateLocalSettingsJson / generateDesignTimeLocalSettingsJson', () =
});
});

it('logicApp with JDBC driver JARs: adds the multi-language worker feature flag', () => {
expect(generateLocalSettingsJson(projectPath, ProjectType.logicApp, { hasJdbcDriverJars: true })).toEqual({
IsEncrypted: false,
Values: {
...baseRootValues,
[azureWebJobsFeatureFlagsKey]: multiLanguageWorkerSetting,
},
});
});

it('customCode: adds the multi-language worker feature flag', () => {
expect(generateLocalSettingsJson(projectPath, ProjectType.customCode)).toEqual({
IsEncrypted: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,21 @@ import { isManagedIdentityAuthEnabled } from '../../utils/vsCodeConfig/settings'
import { ProjectType, WorkerRuntime } from '@microsoft/vscode-extension-logic-apps';
import type { ILocalSettingsJson } from '@microsoft/vscode-extension-logic-apps';

export interface LocalSettingsJsonOptions {
hasJdbcDriverJars?: boolean;
}

/**
* Generates the canonical local.settings.json content for a Logic App project.
*
* @param projectPath - The project path (used for ProjectDirectoryPath).
* @param logicAppType - The project type (affects feature flags and codeful settings).
* @param options - Optional project content signals that affect generated settings.
*/
export function generateLocalSettingsJson(
projectPath?: string,
logicAppType?: ProjectType
logicAppType?: ProjectType,
options: LocalSettingsJsonOptions = {}
): ILocalSettingsJson {
const values: Record<string, string> = {};

Expand All @@ -47,7 +53,7 @@ export function generateLocalSettingsJson(
values[workflowAuthenticationMethodKey] = workflowAuthenticationMethodMIValue;
}

if (logicAppType !== undefined && logicAppType !== ProjectType.logicApp) {
if ((logicAppType !== undefined && logicAppType !== ProjectType.logicApp) || options.hasJdbcDriverJars) {
values[azureWebJobsFeatureFlagsKey] = multiLanguageWorkerSetting;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {
ProjectDirectoryPathKey,
appKindSetting,
azureWebJobsFeatureFlagsKey,
connectionsFileName,
designTimeDirectoryName,
extensionBundleId,
Expand All @@ -29,6 +30,7 @@ import {
generateDesignTimeLocalSettingsJson,
} from './fileGenerators';
import { addOrUpdateLocalAppSettings, getLocalSettingsJson } from '../utils/appSettings/localSettings';
import { hasJdbcDriverJars, mergeMultiLanguageWorkerFlag } from '../utils/java/jdbcConnector';
import { writeFormattedJson } from '../utils/fs';
import { parseJson } from '../utils/parseJson';
import { WorkerRuntime } from '@microsoft/vscode-extension-logic-apps';
Expand Down Expand Up @@ -183,7 +185,8 @@ export async function ensureLocalSettingsFile(
const fileExisted = await fse.pathExists(localSettingsPath);

const logicAppType = await detectProjectType(projectPath);
const baselineValues = generateLocalSettingsJson(projectPath, logicAppType).Values ?? {};
const hasJdbcDrivers = await hasJdbcDriverJars(projectPath);
const baselineValues = generateLocalSettingsJson(projectPath, logicAppType, { hasJdbcDriverJars: hasJdbcDrivers }).Values ?? {};
const referencedSettings = await getReferencedAppSettings(projectPath);

const currentSettings: ILocalSettingsJson = await getLocalSettingsJson(context, projectPath);
Expand All @@ -203,6 +206,18 @@ export async function ensureLocalSettingsFile(
}
}

// JDBC/Java built-in connectors run on the Functions multi-language (Java) worker, which is only
// enabled by the AzureWebJobsFeatureFlags=EnableMultiLanguageWorker app setting (issue #8597). The
// generator owns whether that flag belongs in the baseline for this project, while the repair path here
// merges it with any user-defined flags so existing values are never clobbered.
if (hasJdbcDrivers) {
const currentFlags = currentValues[azureWebJobsFeatureFlagsKey] ?? settingsToAdd[azureWebJobsFeatureFlagsKey];
const mergedFlags = mergeMultiLanguageWorkerFlag(currentFlags);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

mergeMultiLanguageWorkerFlag seems like it should be applied in more than just the JDBC scenario, would this always be relevant if azureWebJobsFeatureFlagsKey is in currentValues and settingsToAdd? Other than this lgtm

if (mergedFlags !== currentValues[azureWebJobsFeatureFlagsKey]) {
settingsToAdd[azureWebJobsFeatureFlagsKey] = mergedFlags;
}
}

if (isManagedIdentityAuthEnabled() && currentValues[workflowAuthenticationMethodKey] !== workflowAuthenticationMethodMIValue) {
settingsToAdd[workflowAuthenticationMethodKey] = workflowAuthenticationMethodMIValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { writeFormattedJson } from '../fs';
import { getFunctionsCommand } from '../funcCoreTools/funcVersion';
import { getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settings';
import { getWorkspaceLogicAppFolders } from '../workspace';
import { warnIfJdbcJavaRuntimeMissing } from '../java/jdbcConnector';
import { ensureRootProjectFiles, ensureProjectFiles } from '../../projectConsistency/projectFilesConsistency';
import { delay } from '../delay';
import {
Expand Down Expand Up @@ -256,6 +257,10 @@ export async function startDesignTimeApi(projectPath: string): Promise<void> {
throw new Error(localize('DesignTimeDirectoryError', 'Failed to create design-time directory.'));
}

// If the project uses the JDBC built-in connector (driver JARs present) but no local Java
// runtime is installed, warn the user (non-blocking) that a JDK is a prerequisite (issue #8597).
warnIfJdbcJavaRuntimeMissing(actionContext, projectPath).catch(() => undefined);

const cwd: string = designTimeDirectory.fsPath;
const portArgs = `--port ${designTimeInst.port}`;
ext.outputChannel.appendLog(
Expand Down
Loading
Loading