Skip to content
Merged
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 @@ -133,7 +133,8 @@ export class DotnetPathFinder implements IDotnetPathFinder

else
{
this.executor?.execute(CommandExecutor.makeCommand('env', []), { dotnetInstallToolCacheTtlMs: DOTNET_INFORMATION_CACHE_DURATION_MS }, false)
this.executor?.execute(CommandExecutor.makeCommand('echo', ['$PATH']), { dotnetInstallToolCacheTtlMs: DOTNET_INFORMATION_CACHE_DURATION_MS }, false)

.then((result) =>
{
// Log the default shell state
Expand All @@ -143,7 +144,8 @@ export class DotnetPathFinder implements IDotnetPathFinder

if (!(options.shell === '/bin/bash'))
{
this.executor?.execute(CommandExecutor.makeCommand('env', []), { shell: 'bin/bash', dotnetInstallToolCacheTtlMs: SYS_CMD_SEARCH_CACHE_DURATION_MS }, false)
this.executor?.execute(CommandExecutor.makeCommand('echo', ['$PATH']), { shell: 'bin/bash', dotnetInstallToolCacheTtlMs: SYS_CMD_SEARCH_CACHE_DURATION_MS }, false)

.then((result) =>
{
this.workerContext.eventStream.post(new DotnetFindPathLookupPATH(`Execution Path (Unix Bash): ${result?.stdout}`));
Expand Down
14 changes: 14 additions & 0 deletions vscode-dotnet-runtime-library/src/EventStream/EventStreamEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,16 @@ export class DotnetFindPathNoHostOnRegistry extends DotnetCustomMessageEvent
public readonly eventName = 'DotnetFindPathNoHostOnRegistry';
}

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

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

export class DotnetFindPathOnFileSystem extends DotnetCustomMessageEvent
{
public readonly eventName = 'DotnetFindPathOnFileSystem';
Expand Down Expand Up @@ -1535,6 +1545,10 @@ export class NetInstallerEndExecutionEvent extends DotnetCustomMessageEvent
public readonly eventName = 'NetInstallerEndExecutionEvent';
}

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

export class DotnetInstallLinuxChecks extends DotnetCustomMessageEvent
{
Expand Down
75 changes: 56 additions & 19 deletions vscode-dotnet-runtime-library/src/Utils/CommandExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import
DotnetWSLSecurityError,
EventBasedError,
EventCancellationError,
FailedToRunSudoCommand,
SudoDirCreationFailed,
SudoProcAliveCheckBegin,
SudoProcAliveCheckEnd,
SudoProcCommandExchangeBegin,
Expand Down Expand Up @@ -68,12 +70,15 @@ export class CommandExecutor extends ICommandExecutor
LANGUAGE: 'en',
DOTNET_CLI_UI_LANGUAGE: 'en-US',
}; // Not all systems have english installed -- not sure if it's safe to use this.
private sudoProcessCommunicationDir = path.join(__dirname, 'install scripts');
private sudoProcessScript = path.join(__dirname, 'install scripts', 'interprocess-communicator.sh');
private sudoProcessCommunicationDir: string;
private fileUtil: IFileUtilities;

constructor(context: IAcquisitionWorkerContext, utilContext: IUtilityContext, protected readonly validSudoCommands?: string[])
{
super(context, utilContext);

this.sudoProcessCommunicationDir = path.join(__dirname, LockUsedByThisInstanceSingleton.SUDO_SESSION_ID);
this.fileUtil = new FileUtilities();
}

Expand All @@ -85,7 +90,31 @@ export class CommandExecutor extends ICommandExecutor
{
const fullCommandString = CommandExecutor.prettifyCommandExecutorCommand(command, false);
this.context?.eventStream.post(new CommandExecutionUnderSudoEvent(`The command ${fullCommandString} is being ran under sudo.`));
const shellScript = path.join(this.sudoProcessCommunicationDir, 'interprocess-communicator.sh');
const shellScript = this.sudoProcessScript;

try
{
await fs.promises.mkdir(this.sudoProcessCommunicationDir, { recursive: true });
}
catch (error: any)
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
error.message = error?.message + `\nFailed to create ${this.sudoProcessCommunicationDir}. Please check your permissions or install dotnet manually.`;
this.context?.eventStream.post(new SudoDirCreationFailed(`The command ${fullCommandString} failed, as no directory could be made: ${JSON.stringify(error)}`));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.code !== 'EEXIST')
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.code === 'EPERM' || error?.code === 'EACCES')
{
this.sudoProcessCommunicationDir = path.dirname(this.sudoProcessScript);
}
else
{
throw error;
}
}
}

if (await isRunningUnderWSL(this.context, this.utilityContext, this))
{
Expand All @@ -104,7 +133,7 @@ Please install the .NET SDK manually by following https://learn.microsoft.com/en

const waitForLockTimeMs = this.context?.timeoutSeconds ? (this.context?.timeoutSeconds * 1000 / 3) : 180000;
// @ts-expect-error We want to hold the lock and sometimes return a bool, sometimes a CommandExecutorResult. The bool will never be returned if runCommand is true, so this makes the compiler accept this (its bad ik).
return executeWithLock(this.context.eventStream, false, RUN_UNDER_SUDO_LOCK(this.sudoProcessCommunicationDir), SUDO_LOCK_PING_DURATION_MS, waitForLockTimeMs,
return executeWithLock(this.context.eventStream, false, RUN_UNDER_SUDO_LOCK(this.sudoProcessScript), SUDO_LOCK_PING_DURATION_MS, waitForLockTimeMs,
async () =>
{
this.startupSudoProc(fullCommandString, shellScript, terminalFailure).catch(() => {});
Expand All @@ -128,14 +157,6 @@ Please install the .NET SDK manually by following https://learn.microsoft.com/en
return '0';
}
}
else
{
if (await this.sudoProcIsLive(false, fullCommandString, 1000)) // If the sudo process was spawned by another instance of code, we do not want to have 2 at once but also do not waste a lot of time checking
// As it should not be in the middle of an operation which may cause it to take a while.
{
return '0';
}
}

// Launch the process under sudo
this.context?.eventStream.post(new CommandExecutionUserAskDialogueEvent(`Prompting user for command ${fullCommandString} under sudo.`));
Expand Down Expand Up @@ -187,7 +208,7 @@ ${stderr}`));
private async sudoProcIsLive(errorIfDead: boolean, fullCommandString: string, maxTimeoutTimeMs?: number, runCommand = false): Promise<boolean | CommandExecutorResult>
{
const processAliveOkSentinelFile = path.join(this.sudoProcessCommunicationDir, 'ok.txt');
const waitForLockTimeMs = maxTimeoutTimeMs ? maxTimeoutTimeMs : this.context?.timeoutSeconds ? (this.context?.timeoutSeconds * 1000 / 5) : 180000;
const waitForLockTimeMs = maxTimeoutTimeMs ? maxTimeoutTimeMs : (this.context?.timeoutSeconds !== undefined ? (Math.max(this.context.timeoutSeconds * 1000 / 5, 100)) : 180000);
const waitForSudoResponseTimeMs = waitForLockTimeMs * 0.75; // Arbitrary, but this should be less than the time to get the lock.

await (this.fileUtil as FileUtilities).wipeDirectory(this.sudoProcessCommunicationDir, this.context?.eventStream, ['.txt']);
Expand Down Expand Up @@ -219,7 +240,7 @@ ${stderr}`));

const isLive = LockUsedByThisInstanceSingleton.getInstance().isCurrentSudoProcCheckAlive();
this.context?.eventStream.post(new SudoProcAliveCheckEnd(`Finished Sudo Process Master: Is Alive? ${isLive}. ${new Date().toISOString()}
maxTimeoutTimeMs: ${maxTimeoutTimeMs} with lockTime ${waitForLockTimeMs} and responseTime ${waitForSudoResponseTimeMs}`));
waitForLockTimeMs: ${waitForLockTimeMs} with lockTime ${waitForLockTimeMs} and responseTime ${waitForSudoResponseTimeMs}`));

// The sudo process spawned by vscode does not exit unless it fails or times out after an hour. We can't await it as we need it to persist.
// If someone cancels the install, we store that error here since this gets awaited to prevent further code statement control flow from executing.
Expand Down Expand Up @@ -279,22 +300,24 @@ ${stderr}`));
this.context?.eventStream.post(new CommandProcessorExecutionBegin(`The command ${commandToExecuteString} was forwarded to the master process to run.`));


const waitTimeMs = this.context?.timeoutSeconds ? (this.context?.timeoutSeconds * 1000) : 600000;
await loopWithTimeoutOnCond(100, waitTimeMs,
const waitTimeMs = this.context?.timeoutSeconds ? (Math.max(this.context?.timeoutSeconds * 1000, 1000)) : 600000;
const sampleRateMs = 100;
await loopWithTimeoutOnCond(sampleRateMs, waitTimeMs,
function ProcessFinishedExecutingAndWroteOutput(): boolean { return fs.existsSync(outputFile) },
function doNothing(): void { ; },
this.context.eventStream,
new SudoProcCommandExchangePing(`Ping : Waiting. ${new Date().toISOString()}`)
new SudoProcCommandExchangePing(`Ping : Waiting, at rate ${sampleRateMs} with timeout ${waitTimeMs} ${new Date().toISOString()}`)
)
.catch(error =>
{
this.context?.eventStream.post(new FailedToRunSudoCommand(`The command ${commandToExecuteString} failed to run: ${JSON.stringify(error ?? '')}.`));
// Let the rejected promise get handled below. This is required to not make an error from the checking if this promise is alive
});

commandOutputJson = {
stdout: (fs.readFileSync(stdoutFile, 'utf8')).trim(),
stderr: (fs.readFileSync(stderrFile, 'utf8')).trim(),
status: (fs.readFileSync(statusFile, 'utf8')).trim()
stdout: (await (this.fileUtil as FileUtilities).read(stdoutFile)).trim(),
stderr: (await (this.fileUtil as FileUtilities).read(stderrFile)).trim(),
Copy link

Choose a reason for hiding this comment

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

This defaults to utf8? I'm also wondering if you can have these three going in parallel then just 'join' them afterwards.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! The function does default to utf8. I like the parallelization idea, though I want to avoid adding that complexity here

status: (await (this.fileUtil as FileUtilities).read(statusFile)).trim()
} as CommandExecutorResult;

this.context?.eventStream.post(new SudoProcCommandExchangeEnd(`Finished or timed out with master process. ${new Date().toISOString()}`));
Expand Down Expand Up @@ -347,6 +370,7 @@ ${stderr}`));
await executeWithLock(this.context.eventStream, false, RUN_UNDER_SUDO_LOCK(this.sudoProcessCommunicationDir), SUDO_LOCK_PING_DURATION_MS, this.context.timeoutSeconds * 1000 / 5,
async () =>
{
await (this.fileUtil as FileUtilities).wipeDirectory(this.sudoProcessCommunicationDir, this.context?.eventStream, ['.txt']);
const processExitFile = path.join(this.sudoProcessCommunicationDir, 'exit.txt');
await (this.fileUtil as FileUtilities).writeFileOntoDisk('', processExitFile, this.context?.eventStream);

Expand All @@ -369,6 +393,19 @@ ${stderr}`));
eventStream.post(new TriedToExitMasterSudoProcess(`Tried to exit sudo master process: exit code ${LockUsedByThisInstanceSingleton.getInstance().hasSpawnedSudoSuccessfullyWithoutDeath()}`));
});

try
{
fs.rmdirSync(this.sudoProcessCommunicationDir, { recursive: true });
}
catch (error: any)
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.code !== 'ENOENT')
{
eventStream.post(new SudoDirCreationFailed(`The command ${this.sudoProcessCommunicationDir} failed to rm the sudo directory: ${JSON.stringify(error)}`));
}
}

return LockUsedByThisInstanceSingleton.getInstance().hasSpawnedSudoSuccessfullyWithoutDeath() ? 1 : 0;
}

Expand Down
12 changes: 10 additions & 2 deletions vscode-dotnet-runtime-library/src/Utils/FileUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,16 @@ export class FileUtilities extends IFileUtilities

public async read(filePath: string): Promise<string>
{
const output = await fs.promises.readFile(filePath, 'utf8');
return output;
try
{
const output = await fs.promises.readFile(filePath, 'utf8');
return output;
}
catch (error: any)
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
throw new Error(`Failed to read file ${filePath}: ${error?.message}`);
}
}

public async exists(filePath: string): Promise<boolean>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export class LockUsedByThisInstanceSingleton
private currentAliveStatus = false;
private sudoError: any = null;

public static readonly SUDO_SESSION_ID = crypto.randomUUID().substring(0, 8);

protected constructor(protected lockStringAndThisVsCodeInstanceOwnsIt: { [lockString: string]: boolean } = {})
{

Expand Down
2 changes: 1 addition & 1 deletion vscode-dotnet-runtime-library/src/Utils/NodeIPCMutex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export class NodeIPCMutex
catch (error: any)
{
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error?.code === 'EADDRINUSE') // We couldn't acquire the lock, even though nobody else is using it.
if (error?.code === 'EADDRINUSE' || error?.code === 'EEXIST' || error?.code === 'EACCESS')
{
if (retries >= maxRetryCountToEndAtRoughlyTimeoutTime)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ export async function loopWithTimeoutOnCond(sampleRatePerMs: number, durationToW
if (conditionToStop())
{
doAfterStop();
return;
return Promise.resolve();
}
eventStream?.post(waitEvent);
await new Promise(waitAndResolve => setTimeout(waitAndResolve, sampleRatePerMs));
}
throw new Error('The promise timed out.');
throw new Error(`The promise timed out at ${durationToWaitBeforeTimeoutMs}.`);
}

/**
Expand Down
4 changes: 4 additions & 0 deletions vscode-dotnet-runtime-library/src/test/unit/TestUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export async function getLinuxSupportedDotnetSDKVersion(context: IAcquisitionWor
{
return '6.0.100';
}
if (distroInfo.version < '22.06')
{
return '9.0.100';
}
if (distroInfo.version < '24.04')
{
return '8.0.100';
Expand Down