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 handler when install script makes an incomplete install #2106

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 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 @@ -36,6 +36,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
{
Expand All @@ -54,13 +56,12 @@ You will need to restart VS Code after these changes. If PowerShell is still not

public async installDotnet(installContext: IDotnetInstallationContext, install: DotnetInstall): Promise<void>
{
const winOS = os.platform() === 'win32';
const installCommand = await this.getInstallCommand(installContext.version, installContext.installDir, installContext.installMode, installContext.architecture);

return new Promise<void>(async (resolve, reject) =>
return executeWithLock(this.eventStream, false, installContext.installDir, async (installContext: IDotnetInstallationContext, install: DotnetInstall) =>
{
try
return new Promise<void>(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)
Expand All @@ -69,6 +70,13 @@ You will need to restart VS Code after these changes. If PowerShell is still not
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())))
Copy link
Member Author

Choose a reason for hiding this comment

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

What if it failed to write all of the binary? might wanna check that executing this also gives a 0 status code like dotnet --info or smth if it does exist.

{
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) =>
Expand Down Expand Up @@ -130,15 +138,15 @@ If you cannot change this flag, try setting a custom existingDotnetPath via the
}
});
}
catch (error: any)
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 looksLikeBadExecutionPolicyError(stderr: string): boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -77,54 +78,6 @@ export class InstallTrackerSingleton
InstallTrackerSingleton.instance.extensionState = extensionState;
}

protected executeWithLock = async <A extends any[], R>(alreadyHoldingLock : boolean, dataKey : string, f: (...args: A) => R, ...args: A): Promise<R> =>
{
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
{
Expand Down Expand Up @@ -176,7 +129,7 @@ export class InstallTrackerSingleton

public async canUninstall(isFinishedInstall : boolean, dotnetInstall : DotnetInstall, allowUninstallUserOnlyInstall = false) : Promise<boolean>
{
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);
Expand All @@ -189,15 +142,15 @@ export class InstallTrackerSingleton

public async uninstallAllRecords() : Promise<void>
{
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);
const remainingInstallingVersions = installingVersions.filter(x => x.dotnetInstall.isGlobal);
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);
Expand All @@ -211,7 +164,7 @@ export class InstallTrackerSingleton
*/
public async getExistingInstalls(getAlreadyInstalledVersion : boolean, alreadyHoldingLock = false) : Promise<InstallRecord[]>
{
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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.`));

Expand Down Expand Up @@ -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<InstallRecord[]>
{
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
Expand Down
63 changes: 63 additions & 0 deletions vscode-dotnet-runtime-library/src/Acquisition/LockUtilities.ts
Original file line number Diff line number Diff line change
@@ -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<A extends any[], R>(eventStream : IEventStream, alreadyHoldingLock : boolean, dataKey : string, f: (...args: A) => R, ...args: A): Promise<R>
{
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;
}
Loading