Allow uninstallation of global .NET Installations (#1897)

* Add failure handling for file integrity check

This is now what is hitting the EPERM and ENOENT errors.
We need to skip the check and see what happens from here, such as allowing to elevate via windows.

This also adds specific handlers for when we fail to download the SDK.

* elevate on windows

* Retry if no permission is available the first time

Need to double check that cancelling is handled properly now that its a promise reject

* respond to linter

* Add API to uninstall any install

* Add tests

* undo bad save

* dont uninstall if there are multiple owners

* add some basic uninstall capability

* allow elevation on mac

* fix promise chain a bit, may need to await still

* add uninstall functionality to linux

* add uninstall on linux

* fix merge

* Add a test

* add ui element to pick uninstall

we need to fix the uninstallglobal functoin logic to print out and such still
then test it

* add some output, need to test and check graveyard

* add uninstall events

* fix build

* Fix uninstall to pop up above window

* ui changes per request

* fix test

* fix merge err

* fix mac test

* fix name option

* fix promise logic

* fix promise code again xd

* Add icons for the extension

* add icons to shield and uninstall

* set minimum vscode version to require new icons

* dont sort if no install exists

* use noninteractive frontend in master process to fix dpk unable to re open stdin on uninstall

* try to exit sudo process master and set env var for stdin uninstall fix

* fix eventstream

* fix detection logic for uninstall

* add -y to uninstall

* fix bug with uninstall

* add version check for when disk is full and version string ui fails

* also kill sudo process on uninstall

* use -f to delete file as they may not exist and we dont want to fail due to this at the end

* add -y to update

* respond to linter

* remove unnecessary img

* respond to linter

* respond to pr feedback

* fix when extension may have null record
This commit is contained in:
Noah Gilson 2024-08-15 15:22:20 -07:00 коммит произвёл GitHub
Родитель 20261b57c3
Коммит 8bc2ecfdc3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
21 изменённых файлов: 370 добавлений и 63 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -45,7 +45,6 @@ install.*
.vscode-test/
*.vsix
vscode-dotnet-runtime-library/install scripts/dotnet-install.*
*/images/*
vscode-dotnet-runtime-extension/LICENSE.txt
vscode-dotnet-sdk-extension/LICENSE.txt
.vscode/launch.json

Просмотреть файл

@ -9,10 +9,34 @@ and this project adheres to [Semantic Versioning].
## [2.1.2] - 2024-08-01
Adds the ability to uninstall a particular runtime.
Adds the ability for users to uninstall things themselves.
Adds the ability to uninstall a global installation that is already managed by the extension.
Adds API for extensions to uninstall a singular runtime rather than all runtimes.
Adds offline support so existing installations can be found when offline.
Adds additional signatures to the release.
Fixes 'Lock' acquisition issues.
Fixes a bug with DEBIAN_FRONTEND when installing an SDK on Ubuntu.
Fixes a bug with offline detection.
Fixed a bug with arm64 windows installation detection logic.
Fixed a bug when preview versions are installed by Visual Studio.
Fixes a bug where apt-get lock can be busy. Fixes a bug where stdin can be busy.
Fixes a bug with installation detection of SDKS.
Updates Axios per CVE release.
Migrates to a newer version of typescript and node under the hood. Requires a newer version of VS Code.
Fixes other minor bugs.
## [2.1.1] - 2024-07-18

Двоичные данные
vscode-dotnet-runtime-extension/images/dotnetIcon.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

2
vscode-dotnet-runtime-extension/package-lock.json сгенерированный
Просмотреть файл

@ -38,7 +38,7 @@
"webpack-cli": "^4.9.1"
},
"engines": {
"vscode": "^1.74.0"
"vscode": "^1.81.1"
}
},
"../vscode-dotnet-runtime-library": {

Просмотреть файл

@ -16,7 +16,7 @@
"version": "2.1.2",
"publisher": "ms-dotnettools",
"engines": {
"vscode": "^1.74.0"
"vscode": "^1.81.1"
},
"categories": [
"Other"
@ -51,6 +51,11 @@
"title": "Install the .NET SDK System-Wide.",
"category": ".NET Install Tool",
"enablement": "!dotnetAcquisitionExtension.isGlobalSDKUnsupported"
},
{
"command": "dotnet.uninstallPublic",
"title": "Uninstall .NET.",
"category": ".NET Install Tool"
}
],
"configuration": {

Просмотреть файл

@ -61,14 +61,17 @@ import {
DotnetInstallType,
DotnetAcquisitionTotalSuccessEvent,
isRunningUnderWSL,
InstallRecord,
getMajor,
getMajorMinor,
DotnetOfflineWarning
DotnetOfflineWarning,
} from 'vscode-dotnet-runtime-library';
import { dotnetCoreAcquisitionExtensionId } from './DotnetCoreAcquisitionId';
import { IDotnetCoreAcquisitionWorker } from 'vscode-dotnet-runtime-library/dist/Acquisition/IDotnetCoreAcquisitionWorker';
import { InstallTrackerSingleton } from 'vscode-dotnet-runtime-library/dist/Acquisition/InstallTrackerSingleton';
// tslint:disable no-var-requires
/* tslint:disable:only-arrow-functions */
const packageJson = require('../package.json');
// Extension constants
@ -79,11 +82,13 @@ namespace configKeys {
export const existingSharedPath = 'sharedExistingDotnetPath'
export const proxyUrl = 'proxyUrl';
}
namespace commandKeys {
export const acquire = 'acquire';
export const acquireGlobalSDK = 'acquireGlobalSDK';
export const acquireStatus = 'acquireStatus';
export const uninstall = 'uninstall';
export const uninstallPublic = 'uninstallPublic'
export const uninstallAll = 'uninstallAll';
export const listVersions = 'listVersions';
export const recommendedVersion = 'recommendedVersion'
@ -307,7 +312,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
globalEventStream.post(new GlobalAcquisitionContextMenuOpened(`The user has opened the global SDK acquisition context menu.`));
const recommendedVersionResult : IDotnetListVersionsResult = await vscode.commands.executeCommand('dotnet.recommendedVersion');
const recommendedVersion : string = recommendedVersionResult ? recommendedVersionResult[0].version : '';
const recommendedVersion : string = recommendedVersionResult ? recommendedVersionResult[0]?.version : '';
const chosenVersion = await vscode.window.showInputBox(
{
@ -354,6 +359,77 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
return pathResult;
});
const dotnetUninstallPublicRegistration = vscode.commands.registerCommand(`${commandPrefix}.${commandKeys.uninstallPublic}`, async () =>
{
const existingInstalls = await InstallTrackerSingleton.getInstance(globalEventStream, vsCodeContext.globalState).getExistingInstalls(true);
const menuItems = existingInstalls?.sort(
function(x : InstallRecord, y : InstallRecord) : number
{
if(x.dotnetInstall.installMode === y.dotnetInstall.installMode)
{
return x.dotnetInstall.version.localeCompare(y.dotnetInstall.version);
}
return x.dotnetInstall.installMode.localeCompare(y.dotnetInstall.installMode);
})?.map(install =>
{
return {
label : `.NET ${(install.dotnetInstall.installMode === 'sdk' ? 'SDK' : install.dotnetInstall.installMode === 'runtime' ? 'Runtime' : 'ASP.NET Core Runtime')} ${install.dotnetInstall.version}`,
description : `${install.dotnetInstall.architecture ?? ''} | ${install.dotnetInstall.isGlobal ? 'machine-wide' : 'vscode-local' }`,
detail : install.installingExtensions.some(x => x !== null) ? `Used by ${install.installingExtensions.join(', ')}` : ``,
iconPath : install.dotnetInstall.isGlobal ? new vscode.ThemeIcon('shield') : new vscode.ThemeIcon('trash'),
internalId : install.dotnetInstall.installId
}
});
if(menuItems.length < 1)
{
vscode.window.showInformationMessage('No .NET installations were found to uninstall.');
return;
}
const chosenVersion = await vscode.window.showQuickPick(menuItems, { placeHolder: 'Select a version to uninstall.' });
if(chosenVersion)
{
const installRecord : InstallRecord = existingInstalls.find(install => install.dotnetInstall.installId === chosenVersion.internalId)!;
if(!installRecord || !installRecord?.dotnetInstall?.version || !installRecord?.dotnetInstall?.installMode)
{
return;
}
const selectedInstall : DotnetInstall = installRecord.dotnetInstall;
let canContinue = true;
const uninstallWillBreakSomething = !(await InstallTrackerSingleton.getInstance(globalEventStream, vsCodeContext.globalState).canUninstall(true, selectedInstall, true));
const yes = `Continue`;
if(uninstallWillBreakSomething)
{
const pick = await vscode.window.showWarningMessage(
`Uninstalling .NET ${selectedInstall.version} will likely cause ${installRecord.installingExtensions.some(x => x !== null) ? installRecord.installingExtensions.join(', ') : 'extensions such as C# or C# DevKit'} to stop functioning properly. Do you still wish to continue?`, { modal: true }, yes);
canContinue = pick === yes;
}
if(!canContinue)
{
return;
}
const commandContext : IDotnetAcquireContext =
{
version: selectedInstall.version,
mode: selectedInstall.installMode,
installType: selectedInstall.isGlobal ? 'global' : 'local',
architecture: selectedInstall.architecture,
requestingExtensionId : 'user'
}
outputChannel.show(true);
return uninstall(commandContext, true);
}
});
/**
* @returns 0 on success. Error string if not.
*/
@ -362,7 +438,7 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
return uninstall(commandContext);
});
async function uninstall(commandContext: IDotnetAcquireContext | undefined) : Promise<string>
async function uninstall(commandContext: IDotnetAcquireContext | undefined, force = false) : Promise<string>
{
let result = '1';
await callWithErrorHandling(async () =>
@ -378,9 +454,13 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
{
const worker = getAcquisitionWorker();
const workerContext = getAcquisitionWorkerContext(commandContext.mode, commandContext);
const versionResolver = new VersionResolver(workerContext);
const resolvedVersion = await versionResolver.getFullVersion(commandContext.version, commandContext.mode);
commandContext.version = resolvedVersion;
if(commandContext.installType === 'local' && !force) // if using force mode, we are also using the UI, which passes the fully specified version to uninstall only
{
const versionResolver = new VersionResolver(workerContext);
const resolvedVersion = await versionResolver.getFullVersion(commandContext.version, commandContext.mode);
commandContext.version = resolvedVersion;
}
const installationId = getInstallIdCustomArchitecture(commandContext.version, commandContext.architecture, commandContext.mode, commandContext.installType);
const install = {installId : installationId, version : commandContext.version, installMode: commandContext.mode, isGlobal: commandContext.installType === 'global',
@ -388,11 +468,12 @@ export function activate(vsCodeContext: vscode.ExtensionContext, extensionContex
if(commandContext.installType === 'local')
{
result = await worker.uninstallLocalRuntimeOrSDK(workerContext, install);
result = await worker.uninstallLocal(workerContext, install, force);
}
else
{
result = await worker.uninstallGlobal(workerContext, install);
const globalInstallerResolver = new GlobalInstallerResolver(workerContext, commandContext.version);
result = await worker.uninstallGlobal(workerContext, install, globalInstallerResolver, force);
}
}
}, getIssueContext(existingPathConfigWorker)(commandContext?.errorConfiguration, 'uninstall'));
@ -591,6 +672,7 @@ We will try to install .NET, but are unlikely to be able to connect to the serve
dotnetListVersionsRegistration,
dotnetRecommendedVersionRegistration,
dotnetUninstallRegistration,
dotnetUninstallPublicRegistration,
dotnetUninstallAllRegistration,
showOutputChannelRegistration,
ensureDependenciesRegistration,

Просмотреть файл

@ -7,7 +7,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"update"
]
},
@ -16,7 +16,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"install",
"-y",
"{packageName}"
@ -29,8 +29,9 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"remove",
"-y",
"{packageName}"
]
}
@ -41,8 +42,9 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"update"
"DPkg::Lock::Timeout=180",
"update",
"-y"
]
},
{
@ -50,7 +52,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"upgrade",
"-y",
"{packageName}"
@ -63,7 +65,7 @@
"commandRoot": "apt-cache",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"search",
"--names-only",
"^{packageName}$"
@ -198,7 +200,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"install",
"-y",
"wget"
@ -226,7 +228,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"update"
]
}
@ -240,7 +242,7 @@
"commandRoot": "apt-get",
"commandParts": [
"-o",
"DPkg::Lock::Timeout=120",
"DPkg::Lock::Timeout=180",
"update"
]
},

Просмотреть файл

@ -3,15 +3,18 @@ EXECFOLDER=$1 # First argument is the working folder as this is launched with cw
TIMEOUT_SEC=$2
OKSIGNALFILE="$EXECFOLDER/ok.txt"
COMMANDTORUNFILE="$EXECFOLDER/command.txt"
EXITFILE="$EXECFOLDER/exit.txt"
#OUTPUTFILE="/home/test_output_.txt"
end=$((SECONDS+3600))
function finish {
rm "$COMMANDTORUNFILE"
rm "$OKSIGNALFILE"
rm -f "$COMMANDTORUNFILE"
rm -f "$OKSIGNALFILE"
}
trap finish EXIT
export DEBIAN_FRONTEND=noninteractive
while true
do
stop=false
@ -41,6 +44,10 @@ do
if test -f "$OKSIGNALFILE"; then
rm "$OKSIGNALFILE"
fi
if test -f "$EXITFILE"; then
rm "$EXITFILE"
exit 0
fi
sleep 5
done
done

Просмотреть файл

@ -33,6 +33,9 @@ import {
EventBasedError,
EventCancellationError,
DotnetInstallationValidated,
DotnetUninstallStarted,
DotnetUninstallCompleted,
DotnetUninstallFailed,
DotnetOfflineInstallUsed,
} from '../EventStream/EventStreamEvents';
@ -411,7 +414,7 @@ To keep your .NET version up to date, please reconnect to the internet at your s
context.eventStream.post(new DotnetAcquisitionPartialInstallation(installId));
// Delete the existing local files so we can re-install. For global installs, let the installer handle it.
await this.uninstallLocalRuntimeOrSDK(context, installId);
await this.uninstallLocal(context, installId);
}
}
@ -423,7 +426,7 @@ To keep your .NET version up to date, please reconnect to the internet at your s
{
context.eventStream.post(new DotnetInstallGraveyardEvent(
`Attempting to remove .NET at ${JSON.stringify(install)} again, as it was left in the graveyard.`));
await this.uninstallLocalRuntimeOrSDK(context, install);
await this.uninstallLocal(context, install);
}
}
@ -533,6 +536,7 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install);
await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).reclassifyInstallingVersionToInstalled(context, install);
await new CommandExecutor(context, this.utilityContext).endSudoProcessMaster(context.eventStream);
context.eventStream.post(new DotnetGlobalAcquisitionCompletionEvent(`The version ${JSON.stringify(install)} completed successfully.`));
return dotnetPath;
}
@ -565,7 +569,7 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install);
if(legacyInstall.dotnetInstall.installId.includes(version))
{
context.eventStream.post(new DotnetLegacyInstallRemovalRequestEvent(`Trying to remove legacy install: ${legacyInstall} of ${version}.`));
await this.uninstallLocalRuntimeOrSDK(context, legacyInstall.dotnetInstall);
await this.uninstallLocal(context, legacyInstall.dotnetInstall);
}
}
}
@ -591,7 +595,7 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install);
}
public async uninstallLocalRuntimeOrSDK(context: IAcquisitionWorkerContext, install : DotnetInstall, force = false) : Promise<string>
public async uninstallLocal(context: IAcquisitionWorkerContext, install : DotnetInstall, force = false) : Promise<string>
{
if(install.isGlobal)
{
@ -612,7 +616,9 @@ ${WinMacGlobalInstaller.InterpretExitCode(installerResult)}`), install);
if(force || await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).canUninstall(true, install))
{
context.eventStream.post(new DotnetUninstallStarted(`Attempting to remove .NET ${install.installId}.`));
this.removeFolderRecursively(context.eventStream, dotnetInstallDir);
context.eventStream.post(new DotnetUninstallCompleted(`Uninstalled .NET ${install.installId}.`));
graveyard.remove(install);
context.eventStream.post(new DotnetInstallGraveyardEvent(`Success at uninstalling ${JSON.stringify(install)} in path ${dotnetInstallDir}`));
}
@ -626,15 +632,45 @@ Other dependents remain.`));
}
catch(error : any)
{
context.eventStream.post(new SuppressedAcquisitionError(error, `The attempt to uninstall .NET ${install} failed - was .NET in use?`));
context.eventStream.post(new SuppressedAcquisitionError(error, `The attempt to uninstall .NET ${install.installId} failed - was .NET in use?`));
return error?.message ?? '1';
}
}
public async uninstallGlobal(context: IAcquisitionWorkerContext, install : DotnetInstall, force = false) : Promise<string>
public async uninstallGlobal(context: IAcquisitionWorkerContext, install : DotnetInstall, globalInstallerResolver : GlobalInstallerResolver, force = false) : Promise<string>
{
// Do nothing right now. Add this in another PR.
return '1';
try
{
context.eventStream.post(new DotnetUninstallStarted(`Attempting to remove .NET ${install.installId}.`));
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
await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).untrackInstallingVersion(context, install, force);
if(force || await InstallTrackerSingleton.getInstance(context.eventStream, context.extensionState).canUninstall(true, install))
{
const installingVersion = await globalInstallerResolver.getFullySpecifiedVersion();
const installer : IGlobalInstaller = os.platform() === 'linux' ?
new LinuxGlobalInstaller(context, this.utilityContext, installingVersion) :
new WinMacGlobalInstaller(context, this.utilityContext, installingVersion, await globalInstallerResolver.getInstallerUrl(), await globalInstallerResolver.getInstallerHash());
const ok = await installer.uninstallSDK(install);
await new CommandExecutor(context, this.utilityContext).endSudoProcessMaster(context.eventStream);
if(ok === '0')
{
context.eventStream.post(new DotnetUninstallCompleted(`Uninstalled .NET ${install.installId}.`));
return '0';
}
}
context.eventStream.post(new DotnetUninstallFailed(`Failed to uninstall .NET ${install.installId}. Uninstall manually or delete the folder.`));
return '117778'; // arbitrary error code to indicate uninstall failed without error.
}
catch(error : any)
{
await new CommandExecutor(context, this.utilityContext).endSudoProcessMaster(context.eventStream);
context.eventStream.post(new SuppressedAcquisitionError(error, `The attempt to uninstall .NET ${install.installId} failed - was .NET in use?`));
return error?.message ?? '1';
}
}
private removeFolderRecursively(eventStream: IEventStream, folderPath: string) {

Просмотреть файл

@ -96,7 +96,7 @@ export class GenericDistroSDKProvider extends IDistroDotnetSDKProvider
command = CommandExecutor.replaceSubstringsInCommands(command, this.missingPackageNameKey, sdkPackage);
const commandResult = (await this.commandRunner.executeMultipleCommands(command))[0];
return commandResult.stdout;
return commandResult.status;
}
public async getInstalledDotnetSDKVersions(): Promise<string[]>

Просмотреть файл

@ -22,6 +22,8 @@ export abstract class IGlobalInstaller {
public abstract installSDK(install : DotnetInstall) : Promise<string>
public abstract uninstallSDK(install : DotnetInstall) : Promise<string>
public abstract getExpectedGlobalSDKPath(specificSDKVersionInstalled : string, installedArch : string) : Promise<string>
public abstract getGlobalSdkVersionsInstalledOnMachine() : Promise<Array<string>>;

Просмотреть файл

@ -158,7 +158,7 @@ export class InstallTrackerSingleton
this.inProgressInstalls.delete(resolvedInstall);
}
public async canUninstall(isFinishedInstall : boolean, dotnetInstall : DotnetInstall) : Promise<boolean>
public async canUninstall(isFinishedInstall : boolean, dotnetInstall : DotnetInstall, allowUninstallUserOnlyInstall = false) : Promise<boolean>
{
return this.executeWithLock( false, this.installedVersionsId, async (id: string, install: DotnetInstall) =>
{
@ -166,7 +166,8 @@ 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 || installRecord[0].installingExtensions.length === 0;
return installRecord.length === 0 || installRecord[0]?.installingExtensions?.length === 0 ||
(allowUninstallUserOnlyInstall && installRecord[0]?.installingExtensions?.length === 1 && installRecord[0]?.installingExtensions?.includes('user'));
}, isFinishedInstall ? this.installedVersionsId : this.installingVersionsId, dotnetInstall);
}

Просмотреть файл

@ -28,6 +28,13 @@ export class LinuxGlobalInstaller extends IGlobalInstaller {
return this.linuxSDKResolver.ValidateAndInstallSDK(this.version);
}
public async uninstallSDK() : Promise<string>
{
await this.linuxSDKResolver.Initialize();
return this.linuxSDKResolver.UninstallSDK(this.version);
}
public async getExpectedGlobalSDKPath(specificSDKVersionInstalled : string, installedArch : string) : Promise<string>
{
await this.linuxSDKResolver.Initialize();

Просмотреть файл

@ -361,6 +361,13 @@ If you would like to contribute to the list of supported distros, please visit:
return String(updateOrRejectState);
}
public async UninstallSDK(fullySpecifiedDotnetVersion : string) : Promise<string>
{
await this.Initialize();
return this.distroSDKProvider!.uninstallDotnet(fullySpecifiedDotnetVersion, 'sdk');
}
/**
* This exposes the class member that may or may not be initialized before execution of this function
* ... so other's can use it. (It is a terrible pattern but used because the ctor cannot be async.)

Просмотреть файл

@ -64,6 +64,8 @@ export class WinMacGlobalInstaller extends IGlobalInstaller {
protected versionResolver : VersionResolver;
public file : IFileUtilities;
protected webWorker : WebRequestWorker;
private invalidIntegrityError = `The integrity of the .NET install file is invalid, or there was no integrity to check and you denied the request to continue with those risks.
We cannot verify our .NET file host at this time. Please try again later or install the SDK manually.`;
constructor(context : IAcquisitionWorkerContext, utilContext : IUtilityContext, installingVersion : string, installerUrl : string,
installerHash : string, executor : ICommandExecutor | null = null)
@ -134,7 +136,7 @@ This report should be made at https://github.com/dotnet/vscode-dotnet-runtime/is
'DotnetConflictingGlobalWindowsInstallError',
`A global install is already on the machine: version ${conflictingVersion}, that conflicts with the requested version.
Please uninstall this version first if you would like to continue.
If Visual Studio is installed, you may need to use the VS Setup Window to uninstall the SDK component.`), getInstallFromContext(this.acquisitionContext));
If Visual Studio is installed, you may need to use the VS Setup Window to uninstall the SDK component.`), install);
this.acquisitionContext.eventStream.post(err);
throw err.error;
}
@ -145,17 +147,16 @@ This report should be made at https://github.com/dotnet/vscode-dotnet-runtime/is
if(!canContinue)
{
const err = new DotnetConflictingGlobalWindowsInstallError(new EventCancellationError('DotnetConflictingGlobalWindowsInstallError',
`The integrity of the .NET install file is invalid, or there was no integrity to check and you denied the request to continue with those risks.
We cannot verify our .NET file host at this time. Please try again later or install the SDK manually.`), getInstallFromContext(this.acquisitionContext));
this.invalidIntegrityError), install);
this.acquisitionContext.eventStream.post(err);
throw err.error;
}
const installerResult : string = await this.executeInstall(installerFile);
return this.handleStatus(installerResult, installerFile);
return this.handleStatus(installerResult, installerFile, install);
}
private async handleStatus(installerResult : string, installerFile : string, allowRetry = true) : Promise<string>
private async handleStatus(installerResult : string, installerFile : string, install : DotnetInstall, allowRetry = true) : Promise<string>
{
const validInstallerStatusCodes = ['0', '1641', '3010']; // Ok, Pending Reboot, + Reboot Starting Now
const noPermissionStatusCodes = ['1', '5', '1260', '2147942405'];
@ -172,14 +173,14 @@ We cannot verify our .NET file host at this time. Please try again later or inst
{
// Special code for when user cancels the install
const err = new DotnetInstallCancelledByUserError(new EventCancellationError('DotnetInstallCancelledByUserError',
`The install of .NET was cancelled by the user. Aborting.`), getInstallFromContext(this.acquisitionContext));
`The install of .NET was cancelled by the user. Aborting.`), install);
this.acquisitionContext.eventStream.post(err);
throw err.error;
}
else if(noPermissionStatusCodes.includes(installerResult) && allowRetry)
{
const retryWithElevationResult = await this.executeInstall(installerFile, true);
return this.handleStatus(retryWithElevationResult, installerFile, false);
return this.handleStatus(retryWithElevationResult, installerFile, install, false);
}
else
{
@ -187,6 +188,39 @@ We cannot verify our .NET file host at this time. Please try again later or inst
}
}
public async uninstallSDK(install : DotnetInstall): Promise<string>
{
if(os.platform() === 'win32')
{
const installerFile : string = await this.downloadInstaller(this.installerUrl);
const canContinue = await this.installerFileHasValidIntegrity(installerFile);
if(!canContinue)
{
const err = new DotnetConflictingGlobalWindowsInstallError(new EventCancellationError('DotnetConflictingGlobalWindowsInstallError',
this.invalidIntegrityError), install);
this.acquisitionContext.eventStream.post(err);
throw err.error;
}
const command = `${path.resolve(installerFile)}`;
const uninstallArgs = ['/uninstall', '/passive', '/norestart'];
const commandResult = await this.commandRunner.execute(CommandExecutor.makeCommand(command, uninstallArgs), {timeout : this.acquisitionContext.timeoutSeconds * 1000});
this.handleTimeout(commandResult);
return commandResult.status;
}
else
{
const command = CommandExecutor.makeCommand(`rm`, [`-rf`, `${path.join(path.dirname(this.getMacPath()), 'sdk', install.version)}`, `&&`,
`rm`, `-rf`, `${path.join(path.dirname(this.getMacPath()), 'sdk-manifests', install.version)}`], true);
const commandResult = await this.commandRunner.execute(command, {timeout : this.acquisitionContext.timeoutSeconds * 1000});
this.handleTimeout(commandResult);
return commandResult.status;
}
}
/**
*
* @param installerUrl the url of the installer to download.
@ -293,9 +327,7 @@ Permissions: ${JSON.stringify(await this.commandRunner.execute(CommandExecutor.m
}
else if(os.platform() === 'darwin')
{
// On an arm machine we would install to /usr/local/share/dotnet/x64/dotnet/sdk` for a 64 bit sdk
// but we don't currently allow customizing the install architecture so that would never happen.
return path.resolve(`/usr/local/share/dotnet/dotnet`);
return this.getMacPath();
}
const err = new DotnetUnexpectedInstallerOSError(new EventBasedError('DotnetUnexpectedInstallerOSError',
@ -317,6 +349,13 @@ If you were waiting for the install to succeed, please extend the timeout settin
}
}
private getMacPath() : string
{
// On an arm machine we would install to /usr/local/share/dotnet/x64/dotnet/sdk` for a 64 bit sdk
// but we don't currently allow customizing the install architecture so that would never happen.
return path.resolve(`/usr/local/share/dotnet/dotnet`);
}
/**
*
* @param installerPath The path to the installer file to run.

Просмотреть файл

@ -731,6 +731,7 @@ export class DotnetAcquisitionDeletion extends DotnetAcquisitionMessage {
export class DotnetFallbackInstallScriptUsed extends DotnetAcquisitionMessage {
public readonly eventName = 'DotnetFallbackInstallScriptUsed';
}
export abstract class DotnetCustomMessageEvent extends DotnetAcquisitionMessage {
constructor(public readonly eventMessage: string) { super(); }
@ -755,11 +756,29 @@ export class DuplicateInstallDetected extends DotnetCustomMessageEvent {
public readonly eventName = 'DuplicateInstallDetected';
}
export class TriedToExitMasterSudoProcess extends DotnetCustomMessageEvent {
public readonly eventName = 'TriedToExitMasterSudoProcess';
}
export class DotnetUninstallStarted extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetUninstallStarted';
public type = EventType.DotnetUninstallMessage;
}
export class DotnetUninstallCompleted extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetUninstallStarted';
public type = EventType.DotnetUninstallMessage;
}
export class DotnetUninstallFailed extends DotnetCustomMessageEvent {
public readonly eventName = 'DotnetUninstallStarted';
public type = EventType.DotnetUninstallMessage;
}
export class NoExtensionIdProvided extends DotnetCustomMessageEvent {
public readonly eventName = 'NoExtensionIdProvided';
}
export class ConvertingLegacyInstallRecord extends DotnetCustomMessageEvent {
public readonly eventName = 'ConvertingLegacyInstallRecord';
}

Просмотреть файл

@ -16,6 +16,7 @@ export enum EventType {
DotnetAcquisitionTest,
DotnetAcquisitionAlreadyInstalled,
DotnetAcquisitionInProgress,
DotnetUninstallMessage,
DotnetDebuggingMessage,
DotnetTotalSuccessEvent,
OfflineInstallUsed,

Просмотреть файл

@ -9,6 +9,7 @@ import {
DotnetAcquisitionError,
DotnetAcquisitionInProgress,
DotnetAcquisitionStarted,
DotnetCustomMessageEvent,
DotnetDebuggingMessage,
DotnetExistingPathResolutionCompleted,
DotnetInstallExpectedAbort,

Просмотреть файл

@ -38,7 +38,8 @@ import {
SudoProcCommandExchangePing,
TimeoutSudoCommandExecutionError,
TimeoutSudoProcessSpawnerError,
EventBasedError
EventBasedError,
TriedToExitMasterSudoProcess
} from '../EventStream/EventStreamEvents';
import {exec as execElevated} from '@vscode/sudo-prompt';
import * as lockfile from 'proper-lockfile';
@ -55,6 +56,7 @@ import { FileUtilities } from './FileUtilities';
import { IFileUtilities } from './IFileUtilities';
import { CommandExecutorResult } from './CommandExecutorResult';
import { isRunningUnderWSL, loopWithTimeoutOnCond } from './TypescriptUtilities';
import { IEventStream } from '../EventStream/EventStream';
/* tslint:disable:no-any */
/* tslint:disable:no-string-literal */
@ -124,8 +126,7 @@ Please install the .NET SDK manually by following https://learn.microsoft.com/en
// Launch the process under sudo
this.context?.eventStream.post(new CommandExecutionUserAskDialogueEvent(`Prompting user for command ${fullCommandString} under sudo.`));
// The '.' character is not allowed for sudo-prompt so we use 'NET'
const options = { name: `${this.getSanitizedCallerName()}` };
const options = { name: this.getSanitizedCallerName() };
fs.chmodSync(shellScriptPath, 0o500);
const timeoutSeconds = Math.max(100, this.context.timeoutSeconds);
@ -172,7 +173,7 @@ ${stderr}`));
const processAliveOkSentinelFile = path.join(this.sudoProcessCommunicationDir, 'ok.txt');
const fakeLockFile = path.join(this.sudoProcessCommunicationDir, 'fakeLockFile'); // We need a file to lock the directory in the API besides the dir lock file
await this.fileUtil.writeFileOntoDisk('', fakeLockFile, false, this.context?.eventStream!);
await (this.fileUtil as FileUtilities).writeFileOntoDisk('', fakeLockFile, false, this.context?.eventStream!);
// Prepare to lock directory
const directoryLock = 'dir.lock';
@ -241,7 +242,7 @@ It had previously spawned: ${this.hasEverLaunchedSudoFork}.`), getInstallFromCon
const outputFile = path.join(this.sudoProcessCommunicationDir, 'output.txt');
const fakeLockFile = path.join(this.sudoProcessCommunicationDir, 'fakeLockFile'); // We need a file to lock the directory in the API besides the dir lock file
await this.fileUtil.writeFileOntoDisk('', fakeLockFile, false, this.context?.eventStream!);
await (this.fileUtil as FileUtilities).writeFileOntoDisk('', fakeLockFile, false, this.context?.eventStream!);
// Prepare to lock directory
const directoryLock = 'dir.lock';
@ -255,9 +256,9 @@ It had previously spawned: ${this.hasEverLaunchedSudoFork}.`), getInstallFromCon
.then(async (release: () => any) =>
{
this.context?.eventStream.post(new DotnetLockAcquiredEvent(`Lock Acquired.`, new Date().toISOString(), directoryLockPath, fakeLockFile));
await this.fileUtil.wipeDirectory(this.sudoProcessCommunicationDir, this.context?.eventStream, ['.txt', '.json']);
(this.fileUtil as FileUtilities).wipeDirectory(this.sudoProcessCommunicationDir, this.context?.eventStream, ['.txt', '.json']);
await this.fileUtil.writeFileOntoDisk(`${commandToExecuteString}`, commandFile, true, this.context?.eventStream!);
await (this.fileUtil as FileUtilities).writeFileOntoDisk(`${commandToExecuteString}`, commandFile, true, this.context?.eventStream!);
this.context?.eventStream.post(new SudoProcCommandExchangeBegin(`Handing command off to master process. ${new Date().toISOString()}`));
this.context?.eventStream.post(new CommandProcessorExecutionBegin(`The command ${commandToExecuteString} was forwarded to the master process to run.`));
@ -324,6 +325,36 @@ ${(commandOutputJson as CommandExecutorResult).stderr}.`),
return commandOutputJson ?? { stdout: '', stderr : '', status: noStatusCodeErrorCode};
}
/**
* @returns 0 if the sudo master process was ended, 1 if it was not.
*/
public async endSudoProcessMaster(eventStream : IEventStream) : Promise<number>
{
let didDelete = 1;
const processExitFile = path.join(this.sudoProcessCommunicationDir, 'exit.txt');
await (this.fileUtil as FileUtilities).writeFileOntoDisk('', processExitFile, true, this.context?.eventStream);
const waitTime = this.context?.timeoutSeconds ? (this.context?.timeoutSeconds * 1000) : 600000;
try
{
await loopWithTimeoutOnCond(100, waitTime,
function processRespondedByDeletingExitFile() : boolean { return !fs.existsSync(processExitFile) },
function returnZeroOnExit() : void { didDelete = 0; },
this.context.eventStream,
new SudoProcCommandExchangePing(`Ping : Waiting to exit sudo process master. ${new Date().toISOString()}`)
);
}
catch(error : any)
{
eventStream.post(new TriedToExitMasterSudoProcess(`Tried to exit sudo master process: FAILED. ${error ? JSON.stringify(error) : ''}`));
}
eventStream.post(new TriedToExitMasterSudoProcess(`Tried to exit sudo master process: exit code ${didDelete}`));
return didDelete;
}
public async executeMultipleCommands(commands: CommandExecutorCommand[], options?: any, terminalFailure = true): Promise<CommandExecutorResult[]>
{
const results = [];
@ -370,13 +401,23 @@ with options ${JSON.stringify(options)}.`));
if(command.runUnderSudo)
{
execElevated(fullCommandString, options, (error?: any, execStdout?: any, execStderr?: any) =>
options.name = this.getSanitizedCallerName();
// tslint:disable no-return-await
return await new Promise<CommandExecutorResult>(async (resolve, reject) =>
{
if(terminalFailure)
execElevated(fullCommandString, options, (error?: any, execStdout?: any, execStderr?: any) =>
{
return Promise.resolve(this.parseVSCodeSudoExecError(error, fullCommandString));
}
return Promise.resolve({ status: error ? error.code : '0', stderr: execStderr, stdout: execStdout} as CommandExecutorResult);
if(error && terminalFailure)
{
return reject(this.parseVSCodeSudoExecError(error, fullCommandString));
}
else if(error)
{
this.context?.eventStream.post(new CommandExecutionStdError(`The command ${fullCommandString} encountered ERROR: ${JSON.stringify(error)}`));
}
return resolve({ status: error ? error.code : '0', stderr: execStderr, stdout: execStdout} as CommandExecutorResult);
});
});
}
@ -552,6 +593,7 @@ Please report this at https://github.com/dotnet/vscode-dotnet-runtime/issues.`),
private getSanitizedCallerName() : string
{
// The '.' character is not allowed for sudo-prompt so we use 'NET'
let sanitizedCallerName = this.context?.acquisitionContext?.requestingExtensionId?.replace(/[^0-9a-z]/gi, ''); // Remove non-alphanumerics per OS requirements
sanitizedCallerName = sanitizedCallerName?.substring(0, 69); // 70 Characters is the maximum limit we can use for the prompt.
return sanitizedCallerName ?? 'NET Install Tool';

Просмотреть файл

@ -37,7 +37,7 @@ suite('Linux Distro Logic Unit Tests', () =>
{
const recVersion = await provider.getRecommendedDotnetVersion(installType);
assert.equal(mockExecutor.attemptedCommand,
'apt-cache -o DPkg::Lock::Timeout=120 search --names-only ^dotnet-sdk-9.0$', 'Searched for the newest package last with regex'); // this may fail if test not exec'd first
'apt-cache -o DPkg::Lock::Timeout=180 search --names-only ^dotnet-sdk-9.0$', 'Searched for the newest package last with regex'); // this may fail if test not exec'd first
// the data is cached so --version may not be executed.
assert.equal(recVersion, '8.0.1xx', 'Resolved the most recent available version : will eventually break if the mock data is not updated');
}
@ -152,7 +152,7 @@ Microsoft.NETCore.App 7.0.5 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]`, std
if(shouldRun)
{
await provider.installDotnet(mockVersion, installType);
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=120 install -y dotnet-sdk-7.0');
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=180 install -y dotnet-sdk-7.0');
}
}).timeout(standardTimeoutTime);
@ -160,7 +160,7 @@ Microsoft.NETCore.App 7.0.5 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]`, std
if(shouldRun)
{
await provider.uninstallDotnet(mockVersion, installType);
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=120 remove dotnet-sdk-7.0');
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=180 remove -y dotnet-sdk-7.0');
}
}).timeout(standardTimeoutTime);
@ -168,7 +168,7 @@ Microsoft.NETCore.App 7.0.5 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]`, std
if(shouldRun)
{
await provider.upgradeDotnet(mockVersion, installType);
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=120 upgrade -y dotnet-sdk-7.0');
assert.equal(mockExecutor.attemptedCommand, 'sudo apt-get -o DPkg::Lock::Timeout=180 upgrade -y dotnet-sdk-7.0');
}
}).timeout(standardTimeoutTime*1000);
});

Просмотреть файл

@ -192,4 +192,37 @@ ${fs.readdirSync(installerDownloadFolder).join(', ')}`);
console.warn('The check for installer file deletion cannot run without elevation.');
}
}).timeout(15000 * 3);
test('It runs the correct uninstall command', async () =>
{
mockExecutor.fakeReturnValue = {stdout: `0`, status: '0', stderr: ''};
installer.cleanupInstallFiles = false;
const install = GetDotnetInstallInfo(mockVersion, 'sdk', 'global', os.arch());
const result = await installer.uninstallSDK(install);
assert.exists(result);
assert.equal(result, '0');
if(os.platform() === 'darwin')
{
assert.isTrue(mockExecutor.attemptedCommand.startsWith('sudo rm'), `It ran the right mac command, sudo rm. Command found: ${mockExecutor.attemptedCommand}`)
assert.isTrue(mockExecutor.attemptedCommand.includes('rf'), 'It used the -rf flag')
}
else if(os.platform() === 'win32')
{
assert.isTrue(fs.existsSync(mockExecutor.attemptedCommand.split(' ')[0]), 'It ran a command to an executable that exists');
if(new FileUtilities().isElevated())
{
assert.include(mockExecutor.attemptedCommand, ' /uninstall /passive /norestart', 'It ran under the hood if it had privileges already');
}
else
{
assert.include(mockExecutor.attemptedCommand, `/uninstall`, 'it tried to uninstall');
}
}
// Rerun install to clean it up.
installer.cleanupInstallFiles = true;
await installer.installSDK(install);
mockExecutor.resetReturnValues();
}).timeout(150000);
});