Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Localize/lang/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@
"19gdw8": "Learn more",
"1A1P5b": "Comment",
"1AFYij": "This template contains one workflow, therefore it is classified as a Workflow.",
"1Ch1Od": "This name is reserved and cannot be used as a workflow name.",
"1D047X": "Required. The value to convert to XML.",
"1Fn5n+": "Required. The URI encoded string.",
"1GWzEL": "No results found for the specified filters",
Expand Down Expand Up @@ -2123,6 +2124,7 @@
"_19gdw8.comment": "Label to learn more about creating a new connection",
"_1A1P5b.comment": "Comment Label",
"_1AFYij.comment": "Content for the toaster for adding a single workflow.",
"_1Ch1Od.comment": "Error message when workflow name matches a reserved project folder name",
"_1D047X.comment": "Required string parameter to be converted using xml function",
"_1Fn5n+.comment": "Required URI encoded string parameter to be converted using uriComponentToBinary function",
"_1GWzEL.comment": "Text displayed when no results are found in the browse grid",
Expand Down Expand Up @@ -3940,6 +3942,7 @@
"_aGxYMY.comment": "Label to clear editor",
"_aGyVJT.comment": "Required number parameter to get number of objects to remove for skip function",
"_aI9W5L.comment": "Accessibility text to inform user this template does not contain connectors",
"_aIk72V.comment": "Warning message when workflow name collides with an existing workflow in the project",
"_aJg1gl.comment": "error message for maximum invalid retry interval",
"_aKf/r+.comment": "Run identifier not found error message",
"_aP1wk9.comment": "Label for the description of a custom 'indexOf' function",
Expand Down Expand Up @@ -5319,6 +5322,7 @@
"aGxYMY": "Clear editor",
"aGyVJT": "Required. The number of objects to remove from the front of Collection. Must be a positive integer.",
"aI9W5L": "This template does not have connectors",
"aIk72V": "A workflow with this name already exists in the selected project.",
"aJg1gl": "Retry policy maximum interval is invalid, must match ISO 8601 duration format",
"aKf/r+": "Specified run identifier not found",
"aP1wk9": "Returns the first index of a value within a string (case-insensitive, invariant culture)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { localize } from '../../../localize';
import { getDependencyTimeout } from '../../utils/binaries';
import { getDependenciesVersion, ensureExtensionBundleHealthy } from '../../utils/bundleFeed';
import { setDotNetCommand } from '../../utils/dotnet/dotnet';
import { executeCommand } from '../../utils/funcCoreTools/cpUtils';
import { setFunctionsCommand } from '../../utils/funcCoreTools/funcVersion';
import { installLSPSDK } from '../../utils/languageServerProtocol';
import { setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion';
Expand Down Expand Up @@ -35,7 +34,7 @@ export async function validateAndInstallBinaries(context: IActionContext) {
async (progress, token) => {
token.onCancellationRequested(() => {
// Handle cancellation logic
executeCommand(ext.outputChannel, undefined, 'echo', 'validateAndInstallBinaries was canceled');
ext.outputChannel.appendLog('validateAndInstallBinaries was canceled');
});

context.telemetry.properties.lastStep = 'getGlobalSetting';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { validateFuncCoreToolsIsLatest } from '../validateFuncCoreToolsIsLatest';
import { useBinariesDependencies, binariesExist } from '../../../utils/binaries';
import {
useBinariesDependencies,
binariesExist,
verifyDependencyIntegrity,
getLatestFunctionCoreToolsVersion,
} from '../../../utils/binaries';
import { isDevContainerWorkspace } from '../../../utils/devContainerUtils';
import { getLocalFuncCoreToolsVersion } from '../../../utils/funcCoreTools/funcVersion';
import { installFuncCoreToolsBinaries } from '../installFuncCoreTools';

// Factory mocks required to break problematic import chains:
// - binaries: circular dep with onboarding.ts
Expand All @@ -10,6 +17,7 @@ vi.mock('../../../utils/binaries', () => ({
useBinariesDependencies: vi.fn(),
binariesExist: vi.fn(),
getLatestFunctionCoreToolsVersion: vi.fn(),
verifyDependencyIntegrity: vi.fn(),
installBinaries: vi.fn(),
getCpuArchitecture: vi.fn(),
}));
Expand Down Expand Up @@ -106,4 +114,33 @@ describe('validateFuncCoreToolsIsLatest', () => {
expect(binariesExist).not.toHaveBeenCalled();
});
});

describe('on-disk integrity behavior', () => {
it('reinstalls when binaries exist but the on-disk integrity check fails', async () => {
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
vi.mocked(useBinariesDependencies).mockResolvedValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(false);

await validateFuncCoreToolsIsLatest('4');

expect(verifyDependencyIntegrity).toHaveBeenCalledWith(expect.anything(), 'FuncCoreTools');
expect(getLocalFuncCoreToolsVersion).not.toHaveBeenCalled();
expect(getLatestFunctionCoreToolsVersion).not.toHaveBeenCalled();
expect(installFuncCoreToolsBinaries).toHaveBeenCalled();
});

it('does not reinstall when binaries exist, integrity passes, and the version is current', async () => {
vi.mocked(isDevContainerWorkspace).mockResolvedValue(false);
vi.mocked(useBinariesDependencies).mockResolvedValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(true);
vi.mocked(getLocalFuncCoreToolsVersion).mockResolvedValue('4.0.0');
vi.mocked(getLatestFunctionCoreToolsVersion).mockResolvedValue('4.0.0');

await validateFuncCoreToolsIsLatest('4');

expect(installFuncCoreToolsBinaries).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { PackageManager, funcDependencyName } from '../../../constants';
import { localize } from '../../../localize';
import { executeOnFunctions } from '../../functionsExtension/executeOnFunctionsExt';
import { binariesExist, getLatestFunctionCoreToolsVersion, useBinariesDependencies } from '../../utils/binaries';
import { binariesExist, getLatestFunctionCoreToolsVersion, useBinariesDependencies, verifyDependencyIntegrity } from '../../utils/binaries';
import { startAllDesignTimeApis, stopAllDesignTimeApis } from '../../utils/codeless/startDesignTimeApi';
import { getFunctionsCommand, getLocalFuncCoreToolsVersion, tryParseFuncVersion } from '../../utils/funcCoreTools/funcVersion';
import { getBrewPackageName } from '../../utils/funcCoreTools/getBrewPackageName';
Expand Down Expand Up @@ -38,14 +38,19 @@ async function validateFuncCoreToolsIsLatestBinaries(majorVersion?: string): Pro

const binaries = await binariesExist(funcDependencyName);
context.telemetry.properties.binariesExist = `${binaries}`;
// Deep-verify the installed files against the on-disk integrity manifest so a corrupt/incomplete
// install (e.g. a removed Function Host DLL) forces a wipe + reinstall instead of failing at startup.
const integrityValid = binaries ? verifyDependencyIntegrity(context, funcDependencyName) : false;
context.telemetry.properties.integrityValid = `${integrityValid}`;

const localVersion: string | null = binaries ? await getLocalFuncCoreToolsVersion() : null;
const hasValidBinaries = binaries && integrityValid;
const localVersion: string | null = hasValidBinaries ? await getLocalFuncCoreToolsVersion() : null;
context.telemetry.properties.localVersion = localVersion ?? 'null';

const newestVersion: string | undefined = binaries ? await getLatestFunctionCoreToolsVersion(context, majorVersion) : undefined;
const isOutdated = binaries && localVersion && newestVersion && semver.gt(newestVersion, localVersion);
const newestVersion: string | undefined = hasValidBinaries ? await getLatestFunctionCoreToolsVersion(context, majorVersion) : undefined;
const isOutdated = hasValidBinaries && localVersion && newestVersion && semver.gt(newestVersion, localVersion);

const shouldInstall = !binaries || localVersion === null || isOutdated;
const shouldInstall = !binaries || !integrityValid || localVersion === null || isOutdated;

if (shouldInstall) {
if (isOutdated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { nodeJsDependencyName } from '../../../../constants';
import { ext } from '../../../../extensionVariables';
import { binariesExist, getLatestNodeJsVersion } from '../../../utils/binaries';
import { binariesExist, getLatestNodeJsVersion, verifyDependencyIntegrity } from '../../../utils/binaries';
import { getLocalNodeJsVersion, getNodeJsCommand, setNodeJsCommand } from '../../../utils/nodeJs/nodeJsVersion';
import { getWorkspaceSetting, updateGlobalSetting } from '../../../utils/vsCodeConfig/settings';
import { installNodeJs } from '../installNodeJs';
Expand Down Expand Up @@ -38,6 +38,7 @@ vi.mock('../../../../extensionVariables', () => ({
vi.mock('../../../utils/binaries', () => ({
binariesExist: vi.fn(),
getLatestNodeJsVersion: vi.fn(),
verifyDependencyIntegrity: vi.fn(() => true),
}));

vi.mock('../../../utils/nodeJs/nodeJsVersion', () => ({
Expand Down Expand Up @@ -81,6 +82,31 @@ describe('validateNodeJsIsLatest', () => {
vi.mocked(vscode.window.showErrorMessage).mockResolvedValue(undefined);
});

it('reinstalls when binaries exist but the on-disk integrity check fails', async () => {
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(false);

await validateNodeJsIsLatest('20');

expect(verifyDependencyIntegrity).toHaveBeenCalledWith(contextRef.current, nodeJsDependencyName);
expect(installNodeJs).toHaveBeenCalledWith(contextRef.current, '20');
expect(contextRef.current.telemetry.properties.integrityValid).toBe('false');
expect(getLocalNodeJsVersion).not.toHaveBeenCalled();
});

it('does not reinstall when binaries exist and the on-disk integrity check passes', async () => {
vi.mocked(getWorkspaceSetting).mockReturnValue(true);
vi.mocked(binariesExist).mockResolvedValue(true);
vi.mocked(verifyDependencyIntegrity).mockReturnValue(true);
vi.mocked(getLocalNodeJsVersion).mockResolvedValue('18.0.0');
vi.mocked(getLatestNodeJsVersion).mockResolvedValue('18.0.0');

await validateNodeJsIsLatest('18');

expect(installNodeJs).not.toHaveBeenCalled();
expect(contextRef.current.telemetry.properties.integrityValid).toBe('true');
});

it('installs without checking GitHub latest version when binaries are missing', async () => {
vi.mocked(binariesExist).mockResolvedValue(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { nodeJsDependencyName } from '../../../constants';
import { ext } from '../../../extensionVariables';
import { localize } from '../../../localize';
import { binariesExist, getLatestNodeJsVersion } from '../../utils/binaries';
import { binariesExist, getLatestNodeJsVersion, verifyDependencyIntegrity } from '../../utils/binaries';
import { getLocalNodeJsVersion, getNodeJsCommand, setNodeJsCommand } from '../../utils/nodeJs/nodeJsVersion';
import { getWorkspaceSetting, updateGlobalSetting } from '../../utils/vsCodeConfig/settings';
import { installNodeJs } from './installNodeJs';
Expand All @@ -23,8 +23,12 @@ export async function validateNodeJsIsLatest(majorVersion?: string): Promise<voi
await setNodeJsCommand();
const binaries = await binariesExist(nodeJsDependencyName);
context.telemetry.properties.binariesExist = `${binaries}`;
// Deep-verify the installed files against the on-disk integrity manifest so a corrupt/incomplete
// install (e.g. a removed file) forces a wipe + reinstall instead of silently failing at runtime.
const integrityValid = binaries ? verifyDependencyIntegrity(context, nodeJsDependencyName) : false;
context.telemetry.properties.integrityValid = `${integrityValid}`;

if (!binaries) {
if (!binaries || !integrityValid) {
await installNodeJs(context, majorVersion);
context.telemetry.properties.binaryCommand = `${getNodeJsCommand()}`;
} else if (showNodeJsWarning) {
Expand Down
Loading
Loading