From ba540943df8e9696988ab01cec5a36479fdfbdd7 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Tue, 28 Jan 2025 13:32:50 -0800 Subject: [PATCH 1/3] Refactor locking logic into its own class so we can use it elsewhere --- .../Acquisition/InstallTrackerSingleton.ts | 63 +++---------------- .../src/Acquisition/LockUtilities.ts | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+), 55 deletions(-) create mode 100644 vscode-dotnet-runtime-library/src/Acquisition/LockUtilities.ts diff --git a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts index d430b483e3..6ba4d9e1c4 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/InstallTrackerSingleton.ts @@ -39,6 +39,7 @@ import { DotnetCoreAcquisitionWorker } from './DotnetCoreAcquisitionWorker'; import { IEventStream } from '../EventStream/EventStream'; import { IExtensionState } from '../IExtensionState'; import { IDotnetAcquireContext } from '..'; +import { executeWithLock } from './LockUtilities'; interface InProgressInstall { @@ -77,54 +78,6 @@ export class InstallTrackerSingleton InstallTrackerSingleton.instance.extensionState = extensionState; } - protected executeWithLock = async (alreadyHoldingLock : boolean, dataKey : string, f: (...args: A) => R, ...args: A): Promise => - { - const trackingLock = `${dataKey}.lock`; - const lockPath = path.join(__dirname, trackingLock); - fs.writeFileSync(lockPath, '', 'utf-8'); - - let returnResult : any; - - try - { - if(alreadyHoldingLock) - { - // eslint-disable-next-line @typescript-eslint/await-thenable - return await f(...(args)); - } - else - { - this.eventStream?.post(new DotnetLockAttemptingAcquireEvent(`Lock Acquisition request to begin.`, new Date().toISOString(), lockPath, lockPath)); - await lockfile.lock(lockPath, { retries: { retries: 10, minTimeout: 5, maxTimeout: 10000 } }) - .then(async (release) => - { - // eslint-disable-next-line @typescript-eslint/await-thenable - returnResult = await f(...(args)); - this.eventStream?.post(new DotnetLockReleasedEvent(`Lock about to be released.`, new Date().toISOString(), lockPath, lockPath)); - return release(); - }) - .catch((e : Error) => - { - // Either the lock could not be acquired or releasing it failed - this.eventStream?.post(new DotnetLockErrorEvent(e, e.message, new Date().toISOString(), lockPath, lockPath)); - }); - } - } - catch(e : any) - { - // Either the lock could not be acquired or releasing it failed - - // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - this.eventStream.post(new DotnetLockErrorEvent(e, e?.message ?? 'Unable to acquire lock to update installation state', new Date().toISOString(), lockPath, lockPath)); - - // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - throw new EventBasedError('DotnetLockErrorEvent', e?.message, e?.stack); - } - - return returnResult; - } public clearPromises() : void { @@ -176,7 +129,7 @@ export class InstallTrackerSingleton public async canUninstall(isFinishedInstall : boolean, dotnetInstall : DotnetInstall, allowUninstallUserOnlyInstall = false) : Promise { - return this.executeWithLock( false, this.installedVersionsId, async (id: string, install: DotnetInstall) => + return executeWithLock( this.eventStream, false, this.installedVersionsId, async (id: string, install: DotnetInstall) => { this.eventStream.post(new RemovingVersionFromExtensionState(`Removing ${JSON.stringify(install)} with id ${id} from the state.`)); const existingInstalls = await this.getExistingInstalls(id === this.installedVersionsId, true); @@ -189,7 +142,7 @@ export class InstallTrackerSingleton public async uninstallAllRecords() : Promise { - await this.executeWithLock( false, this.installingVersionsId, async () => + await executeWithLock( this.eventStream, false, this.installingVersionsId, async () => { // This does not uninstall global things yet, so don't remove their ids. const installingVersions = await this.getExistingInstalls(false, true); @@ -197,7 +150,7 @@ export class InstallTrackerSingleton await this.extensionState.update(this.installingVersionsId, remainingInstallingVersions); }, ); - return this.executeWithLock( false, this.installedVersionsId, async () => + return executeWithLock( this.eventStream, false, this.installedVersionsId, async () => { const installedVersions = await this.getExistingInstalls(true, true); const remainingInstalledVersions = installedVersions.filter(x => x.dotnetInstall.isGlobal); @@ -211,7 +164,7 @@ export class InstallTrackerSingleton */ public async getExistingInstalls(getAlreadyInstalledVersion : boolean, alreadyHoldingLock = false) : Promise { - return this.executeWithLock( alreadyHoldingLock, getAlreadyInstalledVersion ? this.installedVersionsId : this.installingVersionsId, + return executeWithLock( this.eventStream, alreadyHoldingLock, getAlreadyInstalledVersion ? this.installedVersionsId : this.installingVersionsId, (getAlreadyInstalledVersions : boolean) => { const extensionStateAccessor = getAlreadyInstalledVersions ? this.installedVersionsId : this.installingVersionsId; @@ -284,7 +237,7 @@ ${convertedInstalls.map(x => `${JSON.stringify(x.dotnetInstall)} owned by ${x.in protected async removeVersionFromExtensionState(context : IAcquisitionWorkerContext, idStr: string, installIdObj: DotnetInstall, forceUninstall = false) { - return this.executeWithLock( false, idStr, async (id: string, install: DotnetInstall) => + return executeWithLock( this.eventStream, false, idStr, async (id: string, install: DotnetInstall) => { this.eventStream.post(new RemovingVersionFromExtensionState(`Removing ${JSON.stringify(install)} with id ${id} from the state.`)); const existingInstalls = await this.getExistingInstalls(id === this.installedVersionsId, true); @@ -332,7 +285,7 @@ ${installRecord.map(x => `${x.installingExtensions.join(' ')} ${JSON.stringify(I protected async addVersionToExtensionState(context : IAcquisitionWorkerContext, idStr: string, installObj: DotnetInstall, alreadyHoldingLock = false) { - return this.executeWithLock( alreadyHoldingLock, idStr, async (id: string, install: DotnetInstall) => + return executeWithLock( this.eventStream, alreadyHoldingLock, idStr, async (id: string, install: DotnetInstall) => { this.eventStream.post(new RemovingVersionFromExtensionState(`Adding ${JSON.stringify(install)} with id ${id} from the state.`)); @@ -369,7 +322,7 @@ ${existingVersions.map(x => `${JSON.stringify(x.dotnetInstall)} owned by ${x.ins public async checkForUnrecordedLocalSDKSuccessfulInstall(context : IAcquisitionWorkerContext, dotnetInstallDirectory: string, installedInstallIdsList: InstallRecord[]): Promise { - return this.executeWithLock( false, this.installedVersionsId, async (dotnetInstallDir: string, installedInstallIds: InstallRecord[]) => + return executeWithLock( this.eventStream, false, this.installedVersionsId, async (dotnetInstallDir: string, installedInstallIds: InstallRecord[]) => { let localSDKDirectoryIdIter = ''; try diff --git a/vscode-dotnet-runtime-library/src/Acquisition/LockUtilities.ts b/vscode-dotnet-runtime-library/src/Acquisition/LockUtilities.ts new file mode 100644 index 0000000000..c470ecdd28 --- /dev/null +++ b/vscode-dotnet-runtime-library/src/Acquisition/LockUtilities.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- +* Licensed to the .NET Foundation under one or more agreements. +* The .NET Foundation licenses this file to you under the MIT license. +*--------------------------------------------------------------------------------------------*/ +import * as fs from 'fs'; +import * as lockfile from 'proper-lockfile'; +import * as path from 'path'; +import { + DotnetLockAttemptingAcquireEvent, + DotnetLockErrorEvent, + DotnetLockReleasedEvent, + EventBasedError, +} from '../EventStream/EventStreamEvents'; +import { IEventStream } from '../EventStream/EventStream'; + +export async function executeWithLock(eventStream : IEventStream, alreadyHoldingLock : boolean, dataKey : string, f: (...args: A) => R, ...args: A): Promise +{ + const trackingLock = `${dataKey}.lock`; + const lockPath = path.join(__dirname, trackingLock); + fs.writeFileSync(lockPath, '', 'utf-8'); + + let returnResult : any; + + try + { + if(alreadyHoldingLock) + { + // eslint-disable-next-line @typescript-eslint/await-thenable + return await f(...(args)); + } + else + { + eventStream?.post(new DotnetLockAttemptingAcquireEvent(`Lock Acquisition request to begin.`, new Date().toISOString(), lockPath, lockPath)); + await lockfile.lock(lockPath, { retries: { retries: 10, minTimeout: 5, maxTimeout: 10000 } }) + .then(async (release) => + { + // eslint-disable-next-line @typescript-eslint/await-thenable + returnResult = await f(...(args)); + eventStream?.post(new DotnetLockReleasedEvent(`Lock about to be released.`, new Date().toISOString(), lockPath, lockPath)); + return release(); + }) + .catch((e : Error) => + { + // Either the lock could not be acquired or releasing it failed + eventStream?.post(new DotnetLockErrorEvent(e, e.message, new Date().toISOString(), lockPath, lockPath)); + }); + } + } + catch(e : any) + { + // Either the lock could not be acquired or releasing it failed + + // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + eventStream.post(new DotnetLockErrorEvent(e, e?.message ?? 'Unable to acquire lock to update installation state', new Date().toISOString(), lockPath, lockPath)); + + // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + throw new EventBasedError('DotnetLockErrorEvent', e?.message, e?.stack); + } + + return returnResult; +} From f320a6210cdbabac256fe6f08785956d6b160d92 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Tue, 28 Jan 2025 13:40:03 -0800 Subject: [PATCH 2/3] Add a mutex for the local installation directory and then wipe it if needed --- .../src/Acquisition/AcquisitionInvoker.ts | 119 ++++++++++-------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts b/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts index ff4e832f87..4cef918f22 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts @@ -33,6 +33,8 @@ import { IInstallScriptAcquisitionWorker } from './IInstallScriptAcquisitionWork import { DotnetInstall } from './DotnetInstall'; import { DotnetInstallMode } from './DotnetInstallMode'; import { WebRequestWorker } from '../Utils/WebRequestWorker'; +import { executeWithLock } from './LockUtilities'; +import { getDotnetExecutable } from '../Utils/TypescriptUtilities'; export class AcquisitionInvoker extends IAcquisitionInvoker { protected readonly scriptWorker: IInstallScriptAcquisitionWorker; @@ -49,75 +51,84 @@ You will need to restart VS Code after these changes. If PowerShell is still not public async installDotnet(installContext: IDotnetInstallationContext, install : DotnetInstall): Promise { - const winOS = os.platform() === 'win32'; - const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture); - - return new Promise(async (resolve, reject) => + return executeWithLock(this.eventStream, false, installContext.installDir, async (installContext: IDotnetInstallationContext, install : DotnetInstall) => { - try + return new Promise(async (resolve, reject) => { - let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy unrestricted -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`; - if(winOS) + try { - const powershellReference = await this.verifyPowershellCanRun(installContext, install); - windowsFullCommand = windowsFullCommand.replace('powershell.exe', powershellReference); - } + const winOS = os.platform() === 'win32'; + const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture); - cp.exec(winOS ? windowsFullCommand : installCommand, - { cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' }, - async (error, stdout, stderr) => - { - if (stdout) + let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy unrestricted -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`; + if(winOS) { - this.eventStream.post(new DotnetAcquisitionScriptOutput(install, TelemetryUtilities.HashAllPaths(stdout))); + const powershellReference = await this.verifyPowershellCanRun(installContext, install); + windowsFullCommand = windowsFullCommand.replace('powershell.exe', powershellReference); } - if (stderr) + + // The install script can leave behind a directory in an invalid install state. Make sure the executable is present at the very least. + if(this.fileUtilities.existsSync(installContext.installDir) && !this.fileUtilities.existsSync(path.join(installContext.installDir, getDotnetExecutable()))) { - this.eventStream.post(new DotnetAcquisitionScriptOutput(install, `STDERR: ${TelemetryUtilities.HashAllPaths(stderr)}`)); + this.fileUtilities.wipeDirectory(installContext.installDir, this.eventStream); } - if (error) + + cp.exec(winOS ? windowsFullCommand : installCommand, + { cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' }, + async (error, stdout, stderr) => { - if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream))) + if (stdout) { - const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET'); - this.eventStream.post(new DotnetOfflineFailure(offlineError, install)); - reject(offlineError); + this.eventStream.post(new DotnetAcquisitionScriptOutput(install, TelemetryUtilities.HashAllPaths(stdout))); } - else if (error.signal === 'SIGKILL') { - const newError = new EventBasedError('DotnetAcquisitionTimeoutError', - `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack); - this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds)); - reject(newError); + if (stderr) + { + this.eventStream.post(new DotnetAcquisitionScriptOutput(install, `STDERR: ${TelemetryUtilities.HashAllPaths(stderr)}`)); + } + if (error) + { + if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream))) + { + const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET'); + this.eventStream.post(new DotnetOfflineFailure(offlineError, install)); + reject(offlineError); + } + else if (error.signal === 'SIGKILL') { + const newError = new EventBasedError('DotnetAcquisitionTimeoutError', + `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack); + this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds)); + reject(newError); + } + else + { + const newError = new EventBasedError('DotnetAcquisitionInstallError', + `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack); + this.eventStream.post(new DotnetAcquisitionInstallError(newError, install)); + reject(newError); + } + } + else if (stderr && stderr.length > 0) + { + this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); + resolve(); } else { - const newError = new EventBasedError('DotnetAcquisitionInstallError', - `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack); - this.eventStream.post(new DotnetAcquisitionInstallError(newError, install)); - reject(newError); + this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); + resolve(); } - } - else if (stderr && stderr.length > 0) - { - this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); - resolve(); - } - else - { - this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); - resolve(); - } - }); - } - catch (error : any) - { - // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const newError = new EventBasedError('DotnetAcquisitionUnexpectedError', error?.message, error?.stack) - this.eventStream.post(new DotnetAcquisitionUnexpectedError(newError, install)); - reject(newError); - } - }); + }); + } + catch (error : any) + { + // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const newError = new EventBasedError('DotnetAcquisitionUnexpectedError', error?.message, error?.stack) + this.eventStream.post(new DotnetAcquisitionUnexpectedError(newError, install)); + reject(newError); + } + }); + }, installContext, install ); } private async getInstallCommand(version: string, dotnetInstallDir: string, installMode: DotnetInstallMode, architecture: string): Promise { From 4da19c627eed90c1ab8969daebcd456e301528b3 Mon Sep 17 00:00:00 2001 From: Noah Gilson Date: Thu, 13 Feb 2025 12:03:26 -0800 Subject: [PATCH 3/3] Fix some of the logic --- .../src/Acquisition/AcquisitionInvoker.ts | 161 +++++++++--------- 1 file changed, 82 insertions(+), 79 deletions(-) diff --git a/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts b/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts index 86022d2177..78d11f90ef 100644 --- a/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts +++ b/vscode-dotnet-runtime-library/src/Acquisition/AcquisitionInvoker.ts @@ -22,22 +22,22 @@ import PowershellBadLanguageMode, } from '../EventStream/EventStreamEvents'; -import { timeoutConstants } from '../Utils/ErrorHandler' -import { InstallScriptAcquisitionWorker } from './InstallScriptAcquisitionWorker'; import { TelemetryUtilities } from '../EventStream/TelemetryUtilities'; -import { FileUtilities } from '../Utils/FileUtilities'; import { CommandExecutor } from '../Utils/CommandExecutor'; +import { timeoutConstants } from '../Utils/ErrorHandler'; +import { FileUtilities } from '../Utils/FileUtilities'; +import { InstallScriptAcquisitionWorker } from './InstallScriptAcquisitionWorker'; import { IUtilityContext } from '../Utils/IUtilityContext'; -import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; +import { getDotnetExecutable } from '../Utils/TypescriptUtilities'; +import { WebRequestWorker } from '../Utils/WebRequestWorker'; +import { DotnetInstall } from './DotnetInstall'; +import { DotnetInstallMode } from './DotnetInstallMode'; import { IAcquisitionInvoker } from './IAcquisitionInvoker'; +import { IAcquisitionWorkerContext } from './IAcquisitionWorkerContext'; import { IDotnetInstallationContext } from './IDotnetInstallationContext'; import { IInstallScriptAcquisitionWorker } from './IInstallScriptAcquisitionWorker'; -import { DotnetInstall } from './DotnetInstall'; -import { DotnetInstallMode } from './DotnetInstallMode'; -import { WebRequestWorker } from '../Utils/WebRequestWorker'; import { executeWithLock } from './LockUtilities'; -import { getDotnetExecutable } from '../Utils/TypescriptUtilities'; export class AcquisitionInvoker extends IAcquisitionInvoker { @@ -60,92 +60,95 @@ You will need to restart VS Code after these changes. If PowerShell is still not { return new Promise(async (resolve, reject) => { - const winOS = os.platform() === 'win32'; - const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture); - let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy bypass -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`; - let powershellReference = 'powershell.exe'; - if (winOS) + try { - powershellReference = await this.verifyPowershellCanRun(installContext, install); - windowsFullCommand = windowsFullCommand.replace('powershell.exe', powershellReference); - } + const winOS = os.platform() === 'win32'; + const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture); + let windowsFullCommand = `powershell.exe -NoProfile -NonInteractive -NoLogo -ExecutionPolicy bypass -Command "& { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12; & ${installCommand} }"`; + let powershellReference = 'powershell.exe'; + if (winOS) + { + powershellReference = await this.verifyPowershellCanRun(installContext, install); + windowsFullCommand = windowsFullCommand.replace('powershell.exe', powershellReference); + } - // The install script can leave behind a directory in an invalid install state. Make sure the executable is present at the very least. - if (this.fileUtilities.existsSync(installContext.installDir) && !this.fileUtilities.existsSync(path.join(installContext.installDir, getDotnetExecutable()))) - { - this.fileUtilities.wipeDirectory(installContext.installDir, this.eventStream); - } - - cp.exec(winOS ? windowsFullCommand : installCommand, - { cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' }, - async (error, stdout, stderr) => + // The install script can leave behind a directory in an invalid install state. Make sure the executable is present at the very least. + if (this.fileUtilities.existsSync(installContext.installDir) && !this.fileUtilities.existsSync(path.join(installContext.installDir, getDotnetExecutable()))) { - if (stdout) - { - this.eventStream.post(new DotnetAcquisitionScriptOutput(install, TelemetryUtilities.HashAllPaths(stdout))); - } - if (stderr) - { - this.eventStream.post(new DotnetAcquisitionScriptOutput(install, `STDERR: ${TelemetryUtilities.HashAllPaths(stderr)}`)); - } - if (this.looksLikeBadExecutionPolicyError(stderr)) + this.fileUtilities.wipeDirectory(installContext.installDir, this.eventStream); + } + + cp.exec(winOS ? windowsFullCommand : installCommand, + { cwd: process.cwd(), maxBuffer: 500 * 1024, timeout: 1000 * installContext.timeoutSeconds, killSignal: 'SIGKILL' }, + async (error, stdout, stderr) => { - const badPolicyError = new EventBasedError('PowershellBadExecutionPolicy', `Your powershell execution policy does not allow script execution, so we can't automate the installation. + if (stdout) + { + this.eventStream.post(new DotnetAcquisitionScriptOutput(install, TelemetryUtilities.HashAllPaths(stdout))); + } + if (stderr) + { + this.eventStream.post(new DotnetAcquisitionScriptOutput(install, `STDERR: ${TelemetryUtilities.HashAllPaths(stderr)}`)); + } + if (this.looksLikeBadExecutionPolicyError(stderr)) + { + const badPolicyError = new EventBasedError('PowershellBadExecutionPolicy', `Your powershell execution policy does not allow script execution, so we can't automate the installation. Please read more at https://go.microsoft.com/fwlink/?LinkID=135170`); - this.eventStream.post(new PowershellBadExecutionPolicy(badPolicyError, install)); - reject(badPolicyError); - } - if ((this.looksLikeBadLanguageModeError(stderr) || error?.code === 1) && this.badLanguageModeSet(powershellReference)) - { - const badModeError = new EventBasedError('PowershellBadLanguageMode', `Your Language Mode disables PowerShell language features needed to install .NET. Read more at: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes. + this.eventStream.post(new PowershellBadExecutionPolicy(badPolicyError, install)); + reject(badPolicyError); + } + if ((this.looksLikeBadLanguageModeError(stderr) || error?.code === 1) && this.badLanguageModeSet(powershellReference)) + { + const badModeError = new EventBasedError('PowershellBadLanguageMode', `Your Language Mode disables PowerShell language features needed to install .NET. Read more at: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_language_modes. If you cannot change this flag, try setting a custom existingDotnetPath via the instructions here: https://github.com/dotnet/vscode-dotnet-runtime/blob/main/Documentation/troubleshooting-runtime.md.`); - this.eventStream.post(new PowershellBadLanguageMode(badModeError, install)); - reject(badModeError); - } - if (error) - { - if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream))) + this.eventStream.post(new PowershellBadLanguageMode(badModeError, install)); + reject(badModeError); + } + if (error) { - const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET'); - this.eventStream.post(new DotnetOfflineFailure(offlineError, install)); - reject(offlineError); + if (!(await WebRequestWorker.isOnline(installContext.timeoutSeconds, this.eventStream))) + { + const offlineError = new EventBasedError('DotnetOfflineFailure', 'No internet connection detected: Cannot install .NET'); + this.eventStream.post(new DotnetOfflineFailure(offlineError, install)); + reject(offlineError); + } + else if (error.signal === 'SIGKILL') + { + const newError = new EventBasedError('DotnetAcquisitionTimeoutError', + `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack); + this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds)); + reject(newError); + } + else + { + const newError = new EventBasedError('DotnetAcquisitionInstallError', + `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack); + this.eventStream.post(new DotnetAcquisitionInstallError(newError, install)); + reject(newError); + } } - else if (error.signal === 'SIGKILL') + else if (stderr && stderr.length > 0) { - const newError = new EventBasedError('DotnetAcquisitionTimeoutError', - `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, KILLED: ${error.killed}`, error.stack); - this.eventStream.post(new DotnetAcquisitionTimeoutError(error, install, installContext.timeoutSeconds)); - reject(newError); + this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); + resolve(); } else { - const newError = new EventBasedError('DotnetAcquisitionInstallError', - `${timeoutConstants.timeoutMessage}, MESSAGE: ${error.message}, CODE: ${error.code}, SIGNAL: ${error.signal}`, error.stack); - this.eventStream.post(new DotnetAcquisitionInstallError(newError, install)); - reject(newError); + this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); + resolve(); } - } - else if (stderr && stderr.length > 0) - { - this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); - resolve(); - } - else - { - this.eventStream.post(new DotnetAcquisitionCompleted(install, installContext.dotnetPath, installContext.version)); - resolve(); - } - }); - } + }); + } catch (error: any) - { - // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const newError = new EventBasedError('DotnetAcquisitionUnexpectedError', error?.message, error?.stack) - this.eventStream.post(new DotnetAcquisitionUnexpectedError(newError, install)); - reject(newError); - } + { + // Remove this when https://github.com/typescript-eslint/typescript-eslint/issues/2728 is done + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const newError = new EventBasedError('DotnetAcquisitionUnexpectedError', error?.message, error?.stack) + this.eventStream.post(new DotnetAcquisitionUnexpectedError(newError, install)); + reject(newError); + } + }) }, installContext, install); }