Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Uninstall Bugs #2166

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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 @@ -16,12 +16,12 @@ import
DotnetAcquisitionStarted,
DotnetAcquisitionStatusResolved,
DotnetAcquisitionStatusUndefined,
DotnetAcquisitionThoughtInstalledButNot,
DotnetBeginGlobalInstallerExecution,
DotnetCompletedGlobalInstallerExecution,
DotnetFakeSDKEnvironmentVariableTriggered,
DotnetGlobalAcquisitionCompletionEvent,
DotnetGlobalVersionResolutionCompletionEvent,
DotnetInstallGraveyardEvent,
DotnetInstallIdCreatedEvent,
DotnetLegacyInstallDetectedEvent,
DotnetLegacyInstallRemovalRequestEvent,
Expand Down Expand Up @@ -68,7 +68,6 @@ import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext';
import { IDotnetCoreAcquisitionWorker } from './IDotnetCoreAcquisitionWorker';
import { IDotnetInstallationContext } from './IDotnetInstallationContext';
import { IGlobalInstaller } from './IGlobalInstaller';
import { InstallationGraveyard } from './InstallationGraveyard';
import
{
InstallRecord,
Expand Down Expand Up @@ -367,7 +366,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);

Expand All @@ -388,15 +386,17 @@ To keep your .NET version up to date, please reconnect to the internet at your s
}
catch (error: any)
{
await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstalledVersion(context, install);
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;
}

if (context.acquisitionContext.installType === 'global')
{
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;
}
}
Expand Down Expand Up @@ -448,18 +448,6 @@ To keep your .NET version up to date, please reconnect to the internet at your s
}
}

public async tryCleanUpInstallGraveyard(context: IAcquisitionWorkerContext): Promise<void>
{
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)
Expand Down Expand Up @@ -628,10 +616,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
Expand All @@ -642,12 +626,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.`));
}

Expand Down Expand Up @@ -690,7 +672,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)
Expand Down Expand Up @@ -718,6 +700,7 @@ Other dependents remain.`));
try
{
await promisify(rimraf)(folderPath);
eventStream.post(new DotnetAcquisitionDeletion(`Deleted .NET folder ${folderPath} when marked for deletion.`));
}
catch (error: any)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,12 @@ export class InstallTrackerSingleton
const existingInstalls = await this.getExistingInstalls(installationState, dirProvider, 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;
}, state, dotnetInstall);
}

Expand Down Expand Up @@ -299,9 +303,9 @@ ${installRecord.map(x => `${x.installingExtensions.join(' ')} ${JSON.stringify(I
return executeWithLock(this.eventStream, alreadyHoldingLock, this.getLockFilePathForKey(context.installDirectoryProvider, installState), 5, 200000,
async (installationState: InstallState, install: DotnetInstall, ctx: IAcquisitionWorkerContext) =>
{
this.eventStream.post(new RemovingVersionFromExtensionState(`Adding ${JSON.stringify(install)} with id ${installationState} from the state.`));
this.eventStream.post(new AddTrackingVersions(`Adding ${JSON.stringify(install)} with id ${id} from the state.`));

const existingVersions = await this.getExistingInstalls(installationState, ctx.installDirectoryProvider, true);
const existingVersions = await this.getExistingInstalls(installState, context.installDirectoryProvider, true);
Copy link
Member Author

Choose a reason for hiding this comment

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

is this ok?

const preExistingInstallIndex = existingVersions.findIndex(x => IsEquivalentInstallation(x.dotnetInstall, install));

if (preExistingInstallIndex !== -1)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1365,15 +1365,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
{
Expand Down Expand Up @@ -1666,6 +1657,10 @@ export class DotnetAcquisitionMissingLinuxDependencies extends DotnetAcquisition
public readonly eventName = 'DotnetAcquisitionMissingLinuxDependencies';
}

export class DotnetAcquisitionThoughtInstalledButNot extends DotnetCustomMessageEvent
{
public readonly eventName = 'DotnetAcquisitionThoughtInstalledButNot';
}

export class DotnetAcquisitionScriptOutput extends DotnetAcquisitionMessage
{
Expand Down
6 changes: 0 additions & 6 deletions vscode-dotnet-runtime-library/src/test/mocks/MockObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { DotnetCoreAcquisitionWorker } from '../../Acquisition/DotnetCoreAcquisitionWorker';
import { GetDotnetInstallInfo } from '../../Acquisition/DotnetInstall';
import { DotnetInstallMode } from '../../Acquisition/DotnetInstallMode';
import { IAcquisitionInvoker } from '../../Acquisition/IAcquisitionInvoker';
import { IAcquisitionWorkerContext } from '../../Acquisition/IAcquisitionWorkerContext';
import { InstallOwner, InstallRecord } from '../../Acquisition/InstallRecord';
import { InstallRecord } from '../../Acquisition/InstallRecord';
import { InstallTrackerSingleton } from '../../Acquisition/InstallTrackerSingleton';
import { IEventStream } from '../../EventStream/EventStream';
import
Expand All @@ -21,7 +20,6 @@ import
DotnetAcquisitionStarted,
DotnetAcquisitionStatusResolved,
DotnetAcquisitionStatusUndefined,
DotnetInstallGraveyardEvent,
DotnetUninstallAllCompleted,
DotnetUninstallAllStarted,
TestAcquireCalled
Expand Down Expand Up @@ -314,43 +312,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<InstallRecord[]>(installingVersionsKey, []), 'We did not hang/ get interrupted during the install.');
assert.deepEqual(extContext.get<InstallRecord[]>(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';
Expand Down
Loading