From 6bf0626928d5e1a9d79f436be892067ebb5c402b Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Tue, 4 Mar 2025 16:29:34 -0800 Subject: [PATCH 1/3] fix typo and collect whether deletion worked --- .../src/Acquisition/DotnetCoreAcquisitionWorker.ts | 1 + .../src/Acquisition/InstallTrackerSingleton.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts index d83a13fa70..844d43e3cc 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts @@ -690,6 +690,7 @@ Other dependents remain.`)); try { await promisify(rimraf)(folderPath); + eventStream.post(new DotnetAcquisitionDeletion(`Deleted .NET folder ${folderPath} when marked for deletion.`)); } catch (error: any) { diff --git a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts index 662f3bba71..f153884c4e 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts @@ -336,7 +336,7 @@ ${installRecord.map(x => `${x.installingExtensions.join(' ')} ${JSON.stringify(I { return this.executeWithLock(alreadyHoldingLock, idStr, async (id: string, install: DotnetInstall) => { - this.eventStream.post(new RemovingVersionFromExtensionState(`Adding ${JSON.stringify(install)} with id ${id} from the state.`)); + this.eventStream.post(new AddTrackingVersions(`Adding ${JSON.stringify(install)} with id ${id} from the state.`)); const existingVersions = await this.getExistingInstalls(id === this.installedVersionsId, true); const preExistingInstallIndex = existingVersions.findIndex(x => IsEquivalentInstallation(x.dotnetInstall, install)); From 00b970e63bfd4ee1e84f396201697242328300d3 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Tue, 4 Mar 2025 17:01:41 -0800 Subject: [PATCH 2/3] Fix some of the deletion code --- .../DotnetCoreAcquisitionWorker.ts | 25 +-- .../Acquisition/InstallTrackerSingleton.ts | 8 +- .../src/Acquisition/InstallationGraveyard.ts | 64 ------ .../src/EventStream/EventStreamEvents.ts | 9 - .../src/test/mocks/MockObjects.ts | 6 - .../unit/DotnetCoreAcquisitionWorker.test.ts | 189 ++++++++---------- 6 files changed, 95 insertions(+), 206 deletions(-) delete mode 100644 vscode-dotnet-runtime-library/src/Acquisition/InstallationGraveyard.ts diff --git a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts index 844d43e3cc..75b46f5a38 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts @@ -21,7 +21,6 @@ import DotnetFakeSDKEnvironmentVariableTriggered, DotnetGlobalAcquisitionCompletionEvent, DotnetGlobalVersionResolutionCompletionEvent, - DotnetInstallGraveyardEvent, DotnetInstallIdCreatedEvent, DotnetLegacyInstallDetectedEvent, DotnetLegacyInstallRemovalRequestEvent, @@ -67,7 +66,6 @@ import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; import { IDotnetCoreAcquisitionWorker } from './IDotnetCoreAcquisitionWorker'; import { IDotnetInstallationContext } from './IDotnetInstallationContext'; import { IGlobalInstaller } from './IGlobalInstaller'; -import { InstallationGraveyard } from './InstallationGraveyard'; import { InstallRecord, @@ -354,7 +352,6 @@ To keep your .NET version up to date, please reconnect to the internet at your s context.installationValidator.validateDotnetInstall(install, dotnetPath); await this.removeMatchingLegacyInstall(context, installedVersions, version); - await this.tryCleanUpInstallGraveyard(context); await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).reclassifyInstallingVersionToInstalled(context, install); @@ -425,18 +422,6 @@ To keep your .NET version up to date, please reconnect to the internet at your s } } - public async tryCleanUpInstallGraveyard(context: IAcquisitionWorkerContext): Promise - { - const graveyard = new InstallationGraveyard(context); - const installsToRemove = await graveyard.get(); - for (const install of installsToRemove) - { - context.eventStream.post(new DotnetInstallGraveyardEvent( - `Attempting to remove .NET at ${JSON.stringify(install)} again, as it was left in the graveyard.`)); - await this.uninstallLocal(context, install); - } - } - private getDefaultInternalArchitecture(existingArch: string | null | undefined) { if (existingArch !== null && existingArch !== undefined) @@ -605,10 +590,6 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install); try { const dotnetInstallDir = context.installDirectoryProvider.getInstallDir(install.installId); - const graveyard = new InstallationGraveyard(context); - - await graveyard.add(install, dotnetInstallDir); - context.eventStream.post(new DotnetInstallGraveyardEvent(`Attempting to remove .NET at ${JSON.stringify(install)} in path ${dotnetInstallDir}`)); await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstalledVersion(context, install, force); // this is the only place where installed and installing could deal with pre existing installing id @@ -619,12 +600,10 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install); context.eventStream.post(new DotnetUninstallStarted(`Attempting to remove .NET ${install.installId}.`)); await this.removeFolderRecursively(context.eventStream, dotnetInstallDir); context.eventStream.post(new DotnetUninstallCompleted(`Uninstalled .NET ${install.installId}.`)); - await graveyard.remove(install); - context.eventStream.post(new DotnetInstallGraveyardEvent(`Success at uninstalling ${JSON.stringify(install)} in path ${dotnetInstallDir}`)); } else { - context.eventStream.post(new DotnetInstallGraveyardEvent(`Removed reference of ${JSON.stringify(install)} in path ${dotnetInstallDir}, but did not uninstall. + context.eventStream.post(new DotnetUninstallFailed(`Removed reference of ${JSON.stringify(install)} in path ${dotnetInstallDir}, but did not uninstall. Other dependents remain.`)); } @@ -663,7 +642,7 @@ Other dependents remain.`)); return '0'; } } - context.eventStream.post(new DotnetUninstallFailed(`Failed to uninstall .NET ${install.installId}. Uninstall manually or delete the folder.`)); + context.eventStream.post(new DotnetUninstallFailed(`Failed to uninstall .NET ${install.installId}. Another install may be in progress? Uninstall manually or delete the folder.`)); return '117778'; // arbitrary error code to indicate uninstall failed without error. } catch (error: any) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts index f153884c4e..5b83acbc0d 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts @@ -184,8 +184,12 @@ export class InstallTrackerSingleton const existingInstalls = await this.getExistingInstalls(id === this.installedVersionsId, true); const installRecord = existingInstalls.filter(x => IsEquivalentInstallation(x.dotnetInstall, install)); - return (installRecord?.length ?? 0) === 0 || installRecord[0]?.installingExtensions?.length === 0 || - (allowUninstallUserOnlyInstall && installRecord[0]?.installingExtensions?.length === 1 && installRecord[0]?.installingExtensions?.includes('user')); + const zeroInstalledRecordsLeft = (installRecord?.length ?? 0) === 0; + const installedRecordsLeftButNoOwnersRemain = installRecord[0]?.installingExtensions?.length === 0; + const installWasMadeByUserAndHasNoExtensionDependencies = (allowUninstallUserOnlyInstall && + installRecord[0]?.installingExtensions?.length === 1 && installRecord[0]?.installingExtensions?.includes('user')) + + return zeroInstalledRecordsLeft || installedRecordsLeftButNoOwnersRemain || installWasMadeByUserAndHasNoExtensionDependencies; }, isFinishedInstall ? this.installedVersionsId : this.installingVersionsId, dotnetInstall); } diff --git a/vscode-dotnet-runtime-library/src/Acquisition/InstallationGraveyard.ts b/vscode-dotnet-runtime-library/src/Acquisition/InstallationGraveyard.ts deleted file mode 100644 index cf4382fd04..0000000000 --- a/vscode-dotnet-runtime-library/src/Acquisition/InstallationGraveyard.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- -* Licensed to the .NET Foundation under one or more agreements. -* The .NET Foundation licenses this file to you under the MIT license. -*--------------------------------------------------------------------------------------------*/ -import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; -import { IsEquivalentInstallationFile , DotnetInstall } from './DotnetInstall'; -import { getAssumedInstallInfo } from '../Utils/InstallIdUtilities'; - - -interface LocalDotnetInstall -{ - dotnetInstall: DotnetInstall; - // The string is the path of the install once completed. - path: string; -} - -type LegacyGraveyardOrModernGraveyard = { [installIds: string]: string } | Set - -export class InstallationGraveyard -{ - // The 'graveyard' includes failed uninstall paths and their install key. - // These will become marked for attempted 'garbage collection' at the end of every acquisition. - private readonly installPathsGraveyardKey = 'installPathsGraveyard'; - - constructor(private readonly context : IAcquisitionWorkerContext) - { - - } - - protected async getGraveyard() : Promise> - { - let graveyard = this.context.extensionState.get(this.installPathsGraveyardKey, new Set()); - if(!(graveyard instanceof Set)) - { - graveyard = new Set( - Object.entries(graveyard).map(([key, path]) => ({ dotnetInstall: getAssumedInstallInfo(key, null), path }) as LocalDotnetInstall) - ); - } - - await this.context.extensionState.update(this.installPathsGraveyardKey, graveyard); - return graveyard; - } - - public async get() : Promise> - { - const graveyard = await this.getGraveyard(); - return new Set([...graveyard].map(x => x.dotnetInstall)); - } - - public async add(installId : DotnetInstall, newPath : string) - { - const graveyard = await this.getGraveyard(); - const newGraveyard = graveyard.add({ dotnetInstall: installId, path: newPath } as LocalDotnetInstall); - await this.context.extensionState.update(this.installPathsGraveyardKey, newGraveyard); - } - - public async remove(installId : DotnetInstall) - { - const graveyard = await this.getGraveyard(); - const newGraveyard : Set = new Set([...graveyard].filter(x => !IsEquivalentInstallationFile(x.dotnetInstall, installId))); - await this.context.extensionState.update(this.installPathsGraveyardKey, newGraveyard); - } - -} \ No newline at end of file diff --git a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts index 09cfbcdda8..1be8dde9b7 100644 --- a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts +++ b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts @@ -1322,15 +1322,6 @@ export class DotnetGlobalAcquisitionCompletionEvent extends DotnetCustomMessageE { public readonly eventName = 'DotnetGlobalAcquisitionCompletionEvent'; } -export class DotnetInstallGraveyardEvent extends DotnetCustomMessageEvent -{ - public readonly eventName = 'DotnetInstallGraveyardEvent'; - - public getProperties() - { - return { suppressTelemetry: 'true', ...super.getProperties() }; - } -} export class DotnetAlternativeCommandFoundEvent extends DotnetCustomMessageEvent { diff --git a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts index 434c74a911..7721d92938 100644 --- a/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts +++ b/vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts @@ -17,7 +17,6 @@ import { IDotnetInstallationContext } from '../../Acquisition/IDotnetInstallatio import { IInstallationValidator } from '../../Acquisition/IInstallationValidator'; import { InstallScriptAcquisitionWorker } from '../../Acquisition/InstallScriptAcquisitionWorker'; import { InstallTrackerSingleton } from '../../Acquisition/InstallTrackerSingleton'; -import { InstallationGraveyard } from '../../Acquisition/InstallationGraveyard'; import { DistroVersionPair, DotnetDistroSupportStatus } from '../../Acquisition/LinuxVersionResolver'; import { VersionResolver } from '../../Acquisition/VersionResolver'; import { IEventStream } from '../../EventStream/EventStream'; @@ -110,11 +109,6 @@ export class MockDotnetCoreAcquisitionWorker extends DotnetCoreAcquisitionWorker super(utilityContext, extensionContext); } - public AddToGraveyard(context: IAcquisitionWorkerContext, install: DotnetInstall, installPath: string) - { - new InstallationGraveyard(context).add(install, installPath); - } - public enableNoInstallInvoker() { this.usingNoInstallInvoker = true; diff --git a/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts b/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts index 4782ef27ec..f70812ef46 100644 --- a/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts +++ b/vscode-dotnet-runtime-library/src/test/unit/DotnetCoreAcquisitionWorker.test.ts @@ -4,22 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; +import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import * as fs from 'fs'; import { DotnetCoreAcquisitionWorker } from '../../Acquisition/DotnetCoreAcquisitionWorker'; -import { +import { DotnetInstallMode } from '../../Acquisition/DotnetInstallMode'; +import { IAcquisitionInvoker } from '../../Acquisition/IAcquisitionInvoker'; +import { IAcquisitionWorkerContext } from '../../Acquisition/IAcquisitionWorkerContext'; +import { InstallRecord } from '../../Acquisition/InstallRecord'; +import { InstallTrackerSingleton } from '../../Acquisition/InstallTrackerSingleton'; +import { IEventStream } from '../../EventStream/EventStream'; +import +{ DotnetAcquisitionCompleted, DotnetAcquisitionStarted, DotnetAcquisitionStatusResolved, DotnetAcquisitionStatusUndefined, - DotnetInstallGraveyardEvent, DotnetUninstallAllCompleted, DotnetUninstallAllStarted, TestAcquireCalled } from '../../EventStream/EventStreamEvents'; import { EventType } from '../../EventStream/EventType'; -import { +import { DotnetInstallType } from '../../IDotnetAcquireContext'; +import { LocalMemoryCacheSingleton } from '../../LocalMemoryCacheSingleton'; +import { getInstallIdCustomArchitecture } from '../../Utils/InstallIdUtilities'; +import { getDotnetExecutable } from '../../Utils/TypescriptUtilities'; +import +{ MockAcquisitionInvoker, MockDotnetCoreAcquisitionWorker, MockEventStream, @@ -29,28 +40,19 @@ import { RejectingAcquisitionInvoker, } from '../mocks/MockObjects'; import { getMockAcquisitionContext, getMockAcquisitionWorker } from './TestUtility'; -import { IAcquisitionInvoker } from '../../Acquisition/IAcquisitionInvoker'; -import { InstallOwner, InstallRecord } from '../../Acquisition/InstallRecord'; -import { GetDotnetInstallInfo } from '../../Acquisition/DotnetInstall'; -import { DotnetInstallMode } from '../../Acquisition/DotnetInstallMode'; -import { IAcquisitionWorkerContext } from '../../Acquisition/IAcquisitionWorkerContext'; -import { IEventStream } from '../../EventStream/EventStream'; -import { DotnetInstallType} from '../../IDotnetAcquireContext'; -import { getInstallIdCustomArchitecture } from '../../Utils/InstallIdUtilities'; -import { InstallTrackerSingleton } from '../../Acquisition/InstallTrackerSingleton'; -import { getDotnetExecutable } from '../../Utils/TypescriptUtilities'; -import { LocalMemoryCacheSingleton } from '../../LocalMemoryCacheSingleton'; const assert = chai.assert; chai.use(chaiAsPromised); const expectedTimeoutTime = 9000; -suite('DotnetCoreAcquisitionWorker Unit Tests', function () { +suite('DotnetCoreAcquisitionWorker Unit Tests', function () +{ const installingVersionsKey = 'installing'; const installedVersionsKey = 'installed'; const dotnetFolderName = `.dotnet O'Hare O'Donald`; - this.afterEach(async () => { + this.afterEach(async () => + { // Tear down tmp storage for fresh run InstallTrackerSingleton.getInstance(new MockEventStream(), new MockExtensionContext()).clearPromises(); LocalMemoryCacheSingleton.getInstance().invalidate(); @@ -64,7 +66,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { return [eventStream, extContext]; } - function setupWorker(workerContext : IAcquisitionWorkerContext, eventStream : IEventStream) : [MockDotnetCoreAcquisitionWorker, IAcquisitionInvoker] + function setupWorker(workerContext: IAcquisitionWorkerContext, eventStream: IEventStream): [MockDotnetCoreAcquisitionWorker, IAcquisitionInvoker] { const acquisitionWorker = getMockAcquisitionWorker(workerContext); const invoker = new NoInstallAcquisitionInvoker(eventStream, acquisitionWorker); @@ -72,18 +74,18 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { return [acquisitionWorker, invoker]; } - async function callAcquire(workerContext : IAcquisitionWorkerContext, acquisitionWorker : DotnetCoreAcquisitionWorker, invoker : IAcquisitionInvoker) + async function callAcquire(workerContext: IAcquisitionWorkerContext, acquisitionWorker: DotnetCoreAcquisitionWorker, invoker: IAcquisitionInvoker) { const result = workerContext.acquisitionContext.mode === undefined || workerContext.acquisitionContext.mode === 'runtime' ? - await acquisitionWorker.acquireLocalRuntime(workerContext, invoker) : - workerContext.acquisitionContext.mode === 'sdk' ? await acquisitionWorker.acquireLocalSDK(workerContext, invoker) : - workerContext.acquisitionContext.mode === 'aspnetcore' ? await acquisitionWorker.acquireLocalASPNET(workerContext, invoker) : - {} as { dotnetPath: string }; + await acquisitionWorker.acquireLocalRuntime(workerContext, invoker) : + workerContext.acquisitionContext.mode === 'sdk' ? await acquisitionWorker.acquireLocalSDK(workerContext, invoker) : + workerContext.acquisitionContext.mode === 'aspnetcore' ? await acquisitionWorker.acquireLocalASPNET(workerContext, invoker) : + {} as { dotnetPath: string }; return result; } - function migrateContextToNewInstall(worker : IAcquisitionWorkerContext, newVersion : string, newArch : string | null | undefined) + function migrateContextToNewInstall(worker: IAcquisitionWorkerContext, newVersion: string, newArch: string | null | undefined) { worker.acquisitionContext.version = newVersion; worker.acquisitionContext.architecture = newArch; @@ -91,11 +93,11 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { function getExpectedPath(installId: string, mode: DotnetInstallMode): string { - if(mode === 'runtime' || mode === 'aspnetcore') + if (mode === 'runtime' || mode === 'aspnetcore') { return path.join(dotnetFolderName, installId, getDotnetExecutable()) } - else if(mode === 'sdk') + else if (mode === 'sdk') { return path.join(dotnetFolderName, getDotnetExecutable()); } @@ -107,7 +109,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { exePath: string, eventStream: MockEventStream, context: MockExtensionContext, - mode : DotnetInstallMode = 'runtime') + mode: DotnetInstallMode = 'runtime') { const expectedPath = getExpectedPath(installId, mode); @@ -143,12 +145,13 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { assert.equal(acquireEvent!.context.installDir, path.dirname(expectedPath), 'The acquisition went to the expected installation directory'); } - this.beforeAll(async () => { + this.beforeAll(async () => + { process.env._VSCODE_DOTNET_INSTALL_FOLDER = dotnetFolderName; }); - async function AssertInstall(acquisitionWorker : DotnetCoreAcquisitionWorker, context : MockExtensionContext, eventStream : MockEventStream, version : string, - invoker : IAcquisitionInvoker, workerContext : IAcquisitionWorkerContext) + async function AssertInstall(acquisitionWorker: DotnetCoreAcquisitionWorker, context: MockExtensionContext, eventStream: MockEventStream, version: string, + invoker: IAcquisitionInvoker, workerContext: IAcquisitionWorkerContext) { const installId = getInstallIdCustomArchitecture(workerContext.acquisitionContext.version, workerContext.acquisitionContext.architecture, workerContext.acquisitionContext.mode ?? 'runtime', workerContext.acquisitionContext.installType ?? 'local'); @@ -158,7 +161,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await assertAcquisitionSucceeded(installId, result.dotnetPath, eventStream, context, workerContext.acquisitionContext.mode!); } - async function acquireWithVersion(version : string, mode : DotnetInstallMode) + async function acquireWithVersion(version: string, mode: DotnetInstallMode) { const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext(mode, version, expectedTimeoutTime, eventStream, extContext); @@ -167,7 +170,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await AssertInstall(acquisitionWorker, extContext, eventStream, version, invoker, ctx); } - async function acquireStatus(version : string, mode : DotnetInstallMode, type : DotnetInstallType) + async function acquireStatus(version: string, mode: DotnetInstallMode, type: DotnetInstallType) { const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext(mode, version, expectedTimeoutTime, eventStream, extContext); @@ -187,7 +190,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { assert.exists(resolvedEvent, 'The sdk is resolved'); } - async function repeatAcquisition(version : string, mode : DotnetInstallMode) + async function repeatAcquisition(version: string, mode: DotnetInstallMode) { const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext(mode, version, expectedTimeoutTime, eventStream, extContext); @@ -203,7 +206,7 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { assert.equal(events.length, 1); } - async function acquireAndUninstallAll(version : string, mode : DotnetInstallMode, type : DotnetInstallType) + async function acquireAndUninstallAll(version: string, mode: DotnetInstallMode, type: DotnetInstallType) { const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext(mode, version, expectedTimeoutTime, eventStream, extContext); @@ -225,7 +228,8 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await acquireWithVersion('1.0', 'runtime'); }).timeout(expectedTimeoutTime); - test('Acquire SDK Version', async () => { + test('Acquire SDK Version', async () => + { await acquireWithVersion('5.0', 'sdk'); }).timeout(expectedTimeoutTime); @@ -234,26 +238,31 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await acquireWithVersion('1.0', 'aspnetcore'); }).timeout(expectedTimeoutTime); - test('Acquire SDK Status', async () => { + test('Acquire SDK Status', async () => + { await acquireStatus('5.0', 'sdk', 'local'); }).timeout(expectedTimeoutTime); - test('Acquire Runtime Status', async () => { + test('Acquire Runtime Status', async () => + { await acquireStatus('5.0', 'runtime', 'local'); }).timeout(expectedTimeoutTime); - test('Acquire ASP.NET Runtime Status', async () => { + test('Acquire ASP.NET Runtime Status', async () => + { await acquireStatus('5.0', 'aspnetcore', 'local'); }).timeout(expectedTimeoutTime); - test('Acquire Runtime Version Multiple Times', async () => { + test('Acquire Runtime Version Multiple Times', async () => + { const numAcquisitions = 3; const version = '1.0'; const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext('runtime', version, expectedTimeoutTime, eventStream, extContext); const [acquisitionWorker, invoker] = setupWorker(ctx, eventStream); - for (let i = 0; i < numAcquisitions; i++) { + for (let i = 0; i < numAcquisitions; i++) + { const pathResult = await acquisitionWorker.acquireLocalRuntime(ctx, invoker); const installId = getInstallIdCustomArchitecture(ctx.acquisitionContext.version, ctx.acquisitionContext.architecture, 'runtime', 'local'); await assertAcquisitionSucceeded(installId, pathResult.dotnetPath, eventStream, extContext); @@ -264,7 +273,8 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { assert.lengthOf(acquireEvents, 1); }).timeout(expectedTimeoutTime); - test('Acquire Multiple Versions and UninstallAll', async () => { + test('Acquire Multiple Versions and UninstallAll', async () => + { const versions = ['1.0', '1.1', '2.0', '2.1', '2.2']; const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext('runtime', versions[0], expectedTimeoutTime, eventStream, extContext); @@ -300,42 +310,6 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await acquireAndUninstallAll('6.0', 'sdk', 'local'); }).timeout(expectedTimeoutTime); - test('Graveyard Removes Failed Uninstalls', async () => { - const version = '1.0'; - const [eventStream, extContext] = setupStates(); - const ctx = getMockAcquisitionContext('runtime', version, expectedTimeoutTime, eventStream, extContext); - const [acquisitionWorker, invoker] = setupWorker(ctx, eventStream); - const installId = getInstallIdCustomArchitecture(ctx.acquisitionContext.version, ctx.acquisitionContext.architecture, 'runtime', 'local'); - const install = GetDotnetInstallInfo(version, 'runtime', 'local', os.arch()); - - const res = await acquisitionWorker.acquireLocalRuntime(ctx, invoker); - await assertAcquisitionSucceeded(installId, res.dotnetPath, eventStream, extContext); - acquisitionWorker.AddToGraveyard(ctx, install, 'Not applicable'); - - const versionToKeep = '5.0'; - migrateContextToNewInstall(ctx, versionToKeep, os.arch()); - await acquisitionWorker.acquireLocalRuntime(ctx, invoker); - - assert.exists(eventStream.events.find(event => event instanceof DotnetInstallGraveyardEvent), 'The graveyard tried to uninstall .NET'); - assert.isEmpty(extContext.get(installingVersionsKey, []), 'We did not hang/ get interrupted during the install.'); - assert.deepEqual(extContext.get(installedVersionsKey, []), - [ - { - dotnetInstall: { - architecture: 'x64', - installId: '5.0~x64', - isGlobal: false, - installMode: 'runtime', - version: '5.0', - }, - installingExtensions: [ - 'test' - ] as InstallOwner[], - } - ] as InstallRecord[], - '.NET was successfully uninstalled and cleaned up properly when marked to be.'); - }).timeout(expectedTimeoutTime); - test('Correctly Removes Legacy (No-Architecture) Installs', async () => { const runtimeV5 = '5.0.00'; @@ -363,8 +337,8 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { await AssertInstall(worker, extensionContext, eventStream, runtimeV5, invoker, ctx); // 5.0 legacy runtime should be replaced, but 6.0 runtime should remain, and all SDK items should remain. - let detailedRemainingInstalls : InstallRecord[] = extensionContext.get(installedVersionsKey, []); - let remainingInstalls : string[] = detailedRemainingInstalls.map(x => x.dotnetInstall.installId); + let detailedRemainingInstalls: InstallRecord[] = extensionContext.get(installedVersionsKey, []); + let remainingInstalls: string[] = detailedRemainingInstalls.map(x => x.dotnetInstall.installId); assert.deepStrictEqual(remainingInstalls, ['5.0.00~x64', runtimeV6, sdkV5, sdkV6], 'Only The Requested Legacy Runtime is replaced when new runtime is installed'); @@ -383,19 +357,23 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { 'Only The Requested Legacy SDK is replaced when new SDK is installed'); }).timeout(expectedTimeoutTime * 6); - test('Repeated Runtime Acquisition', async () => { + test('Repeated Runtime Acquisition', async () => + { await repeatAcquisition('1.0', 'runtime'); }).timeout(expectedTimeoutTime); - test('Repeated ASP.NET Acquisition', async () => { + test('Repeated ASP.NET Acquisition', async () => + { await repeatAcquisition('1.0', 'aspnetcore'); }).timeout(expectedTimeoutTime); - test('Repeated SDK Acquisition', async () => { + test('Repeated SDK Acquisition', async () => + { await repeatAcquisition('5.0', 'sdk'); }).timeout(expectedTimeoutTime); - test('Error is Redirected on Acquisition Failure', async () => { + test('Error is Redirected on Acquisition Failure', async () => + { const version = '1.0'; const [eventStream, extContext] = setupStates(); const ctx = getMockAcquisitionContext('runtime', version, expectedTimeoutTime, eventStream, extContext); @@ -405,8 +383,10 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { return assert.isRejected(acquisitionWorker.acquireLocalRuntime(ctx, acquisitionInvoker), '.NET Acquisition Failed: "Rejecting message"'); }).timeout(expectedTimeoutTime); - test('Get Expected Path With Apostrophe In Install path', async () => { - if(os.platform() === 'win32'){ + test('Get Expected Path With Apostrophe In Install path', async () => + { + if (os.platform() === 'win32') + { const installApostropheFolder = `test' for' apostrophe`; const version = '1.0'; const [eventStream, extContext] = setupStates(); @@ -422,22 +402,27 @@ suite('DotnetCoreAcquisitionWorker Unit Tests', function () { } }).timeout(expectedTimeoutTime); - function deleteFolderRecursive(folderPath: string) { - if (fs.existsSync(folderPath)) { - fs.readdirSync(folderPath).forEach((file) => { - const filePath = path.join(folderPath, file); - - if (fs.lstatSync(filePath).isDirectory()) { - // If the item is a directory, recursively call deleteFolderRecursive - deleteFolderRecursive(filePath); - } else { - // If the item is a file, delete it - fs.unlinkSync(filePath); - } - }); - - // After deleting all the files and subfolders, delete the folder itself - fs.rmdirSync(folderPath); + function deleteFolderRecursive(folderPath: string) + { + if (fs.existsSync(folderPath)) + { + fs.readdirSync(folderPath).forEach((file) => + { + const filePath = path.join(folderPath, file); + + if (fs.lstatSync(filePath).isDirectory()) + { + // If the item is a directory, recursively call deleteFolderRecursive + deleteFolderRecursive(filePath); + } else + { + // If the item is a file, delete it + fs.unlinkSync(filePath); + } + }); + + // After deleting all the files and subfolders, delete the folder itself + fs.rmdirSync(folderPath); } - } + } }); From 869ee5c403635cd425a6227e41d13e3b65a9d15b Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Mon, 10 Mar 2025 10:29:03 -0700 Subject: [PATCH 3/3] Add event for when uninstall is external --- .../src/Acquisition/DotnetCoreAcquisitionWorker.ts | 6 +++++- .../src/EventStream/EventStreamEvents.ts | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts index 75b46f5a38..79e43ff59d 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/DotnetCoreAcquisitionWorker.ts @@ -16,6 +16,7 @@ import DotnetAcquisitionStarted, DotnetAcquisitionStatusResolved, DotnetAcquisitionStatusUndefined, + DotnetAcquisitionThoughtInstalledButNot, DotnetBeginGlobalInstallerExecution, DotnetCompletedGlobalInstallerExecution, DotnetFakeSDKEnvironmentVariableTriggered, @@ -369,6 +370,8 @@ To keep your .NET version up to date, please reconnect to the internet at your s } catch (error: any) { + context.eventStream.post(new DotnetAcquisitionThoughtInstalledButNot(`Local Install ${JSON.stringify(install)} at ${dotnetPath} was tracked under installed but it wasn't found. Maybe it got removed externally.`)); + await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstalledVersion(context, install, true); return null; } @@ -376,7 +379,8 @@ To keep your .NET version up to date, please reconnect to the internet at your s { if (!(await this.sdkIsFound(context, context.acquisitionContext.version))) { - await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstalledVersion(context, install); + context.eventStream.post(new DotnetAcquisitionThoughtInstalledButNot(`Global Install ${JSON.stringify(install)} at ${dotnetPath} was tracked under installed but it wasn't found. Maybe it got removed externally.`)); + await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstalledVersion(context, install, true); return null; } } diff --git a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts index 1be8dde9b7..0ad78e9df3 100644 --- a/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts +++ b/vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts @@ -1614,6 +1614,10 @@ export class DotnetAcquisitionMissingLinuxDependencies extends DotnetAcquisition public readonly eventName = 'DotnetAcquisitionMissingLinuxDependencies'; } +export class DotnetAcquisitionThoughtInstalledButNot extends DotnetCustomMessageEvent +{ + public readonly eventName = 'DotnetAcquisitionThoughtInstalledButNot'; +} export class DotnetAcquisitionScriptOutput extends DotnetAcquisitionMessage {