Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
4 changes: 4 additions & 0 deletions Documentation/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ Note: Each VS Code window gets its own extension host log folder, so the returne

This command is only applicable to Linux machines. It attempts to ensure that .NET dependencies are present and, if they are not, installs them or prompts the user to do so. It accepts a [IDotnetEnsureDependenciesContext](https://github.com/dotnet/vscode-dotnet-runtime/blob/main/vscode-dotnet-runtime-library/src/IDotnetEnsureDependenciesContext.ts) object and has a void return type. It is no longer supported but remains to support legacy behavior.

The intended probe shape is `command: <dotnet executable>` with `arguments` set to a `string[]` containing the .NET DLL payload to load and run. For example, the C# extension calls this command with the acquired `dotnet` path and an argument array containing its language server DLL. This lets the command test whether the specific .NET payload needed by the caller can start, and if it fails with a Linux dependency signal, the user is prompted to install missing dependencies.

Passing CLI-only arguments such as `['--info']` runs the .NET CLI information path instead of the caller's payload and can exercise different runtime dependencies. That can be useful for diagnosis, but it is not the intended contract for this legacy command.

### dotnet.reportIssue

This is a **user-facing** command that opens a pre-populated GitHub issue in the browser and copies the issue body to the clipboard. It does not accept parameters and has a void return type.
Expand Down
6 changes: 3 additions & 3 deletions sample/HelloWorldConsoleApp/HelloWorldConsoleApp.deps.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v2.2",
"signature": "da39a3ee5e6b4b0d3255bfef95601890afd80709"
"name": ".NETCoreApp,Version=v10.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v2.2": {
".NETCoreApp,Version=v10.0": {
"HelloWorldConsoleApp/1.0.0": {
"runtime": {
"HelloWorldConsoleApp.dll": {}
Expand Down
Binary file modified sample/HelloWorldConsoleApp/HelloWorldConsoleApp.dll
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"runtimeOptions": {
"tfm": "netcoreapp2.2",
"tfm": "net10.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "2.2.0"
"version": "10.0.0"
},
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
5 changes: 5 additions & 0 deletions sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"title": "Get the .NET runtime acquisition log file path",
"category": "Sample"
},
{
"command": "sample.dotnet.ensureDependencies",
"title": "Call ensureDotnetDependencies with custom dotnet arguments",
"category": "Sample"
},
{
"command": "sample.dotnet.acquireGlobalSDK",
"title": "Install .NET SDK Globally via .NET Install Tool (Former Runtime Extension)",
Expand Down
86 changes: 84 additions & 2 deletions sample/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,28 @@ import
DotnetVersionSpecRequirement,
IDotnetAcquireContext,
IDotnetAcquireResult,
IDotnetEnsureDependenciesContext,
IDotnetFindPathContext,
IDotnetListVersionsResult,
IDotnetLogResult,
} from 'vscode-dotnet-runtime-library';

function parseEnsureDependenciesArguments(input: string): string[]
{
const trimmed = input.trim();
if (trimmed.startsWith('['))
{
const parsed = JSON.parse(trimmed);
if (!Array.isArray(parsed) || parsed.some(arg => typeof arg !== 'string'))
{
throw new Error('Custom arguments JSON must be an array of strings.');
}
return parsed;
}

return trimmed.length === 0 ? [] : trimmed.split(/\s+/);
}

export function activate(context: vscode.ExtensionContext)
{

Expand Down Expand Up @@ -48,8 +65,8 @@ export function activate(context: vscode.ExtensionContext)
{
await vscode.commands.executeCommand('dotnet.showAcquisitionLog');

// Console app requires .NET Core 2.2.0
const commandRes = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquire', { version: '2.2', requestingExtensionId });
// Console app requires .NET 10.
const commandRes = await vscode.commands.executeCommand<IDotnetAcquireResult>('dotnet.acquire', { version: '10.0', requestingExtensionId });
const dotnetPath = commandRes!.dotnetPath;
if (!dotnetPath)
{
Expand Down Expand Up @@ -236,6 +253,70 @@ ${stderr}`);
}
});

const sampleEnsureDependenciesRegistration = vscode.commands.registerCommand('sample.dotnet.ensureDependencies', async () =>
{
const dotnetPath = await vscode.window.showInputBox({
placeHolder: process.platform === 'win32' ? 'C:\\Program Files\\dotnet\\dotnet.exe' : '/usr/bin/dotnet',
value: 'dotnet',
prompt: 'The dotnet command or executable path to run.',
});

if (!dotnetPath)
{
return;
}

const argumentMode = await vscode.window.showQuickPick(['DLL path', 'Custom arguments'], {
placeHolder: 'Choose the argument shape to pass to dotnet.ensureDotnetDependencies.'
});

if (!argumentMode)
{
return;
}

let args: string[];
if (argumentMode === 'DLL path')
{
const dllPath = await vscode.window.showInputBox({
placeHolder: '/path/to/LanguageServer.dll',
prompt: 'The DLL path to pass as the single dotnet argument.',
});

if (!dllPath)
{
return;
}
args = [dllPath];
}
else
{
const customArgs = await vscode.window.showInputBox({
placeHolder: '--info or ["/path/to/app.dll", "--flag"]',
value: '--info',
prompt: 'Arguments to pass to dotnet. Use JSON array syntax if an argument contains spaces.',
});

if (customArgs === undefined)
{
return;
}
args = parseEnsureDependenciesArguments(customArgs);
Comment thread
nagilson marked this conversation as resolved.
Outdated
}

Comment thread
nagilson marked this conversation as resolved.
try
{
await vscode.commands.executeCommand('dotnet.showAcquisitionLog');
const commandContext: IDotnetEnsureDependenciesContext = { command: dotnetPath, arguments: args };
await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', commandContext);
vscode.window.showInformationMessage(`dotnet.ensureDotnetDependencies completed for: ${dotnetPath} ${args.join(' ')}`);
}
catch (error)
{
vscode.window.showErrorMessage((error as Error).toString());
}
});

const sampleGlobalSDKFromRuntimeRegistration = vscode.commands.registerCommand('sample.dotnet.acquireGlobalSDK', async (version: string | undefined) =>
{
if (!version)
Expand Down Expand Up @@ -354,6 +435,7 @@ ${JSON.stringify(result) ?? 'undefined'}`);
sampleConcurrentASPNETTest,
sampleShowAcquisitionLogRegistration,
sampleGetAcquisitionLogRegistration,
sampleEnsureDependenciesRegistration,
sampleFindPathRegistration,
sampleAvailableInstallsRegistration
);
Expand Down
40 changes: 0 additions & 40 deletions sample/yarn.lock
Comment thread
nagilson marked this conversation as resolved.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion vscode-dotnet-runtime-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -911,7 +911,11 @@ ${JSON.stringify(commandContext)}`));
return;
}

const result = cp.spawnSync(commandContext.command, commandContext.arguments);
// commandContext.arguments is either the dotnet process args (string[]) or a SpawnSync options object.
// Use the 3-arg overload (empty args + options) for the options case so the two paths are distinct.
const result = Array.isArray(commandContext.arguments)
? cp.spawnSync(commandContext.command, commandContext.arguments)
: cp.spawnSync(commandContext.command, [], commandContext.arguments);
const installer = new DotnetCoreDependencyInstaller();
if (installer.signalIndicatesMissingLinuxDependencies(result.signal!))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
* The .NET Foundation licenses this file to you under the MIT license.
*--------------------------------------------------------------------------------------------*/
import * as chai from 'chai';
import * as cp from 'child_process';
import { warn } from 'console';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import
{
DotnetCoreDependencyInstaller,
DotnetInstallMode,
DotnetInstallType,
DotnetVersionSpecRequirement,
Expand Down Expand Up @@ -58,6 +60,7 @@ suite('DotnetCoreAcquisitionExtension End to End', function ()
const requestingExtensionId = 'fake.extension';
const mockDisplayWorker = new MockWindowDisplayWorker();
let extensionContext: vscode.ExtensionContext;
let skipInstallCleanupAfterTest = false;
const environmentVariableCollection = new MockEnvironmentVariableCollection();

const existingPathVersionToFake = '5.0.1~x64'
Expand Down Expand Up @@ -116,7 +119,11 @@ suite('DotnetCoreAcquisitionExtension End to End', function ()
process.env.PATH = originalPATH;
LocalMemoryCacheSingleton.getInstance().invalidate();

await vscode.commands.executeCommand<string>('dotnet.uninstallAll');
if (!skipInstallCleanupAfterTest)
{
await vscode.commands.executeCommand<string>('dotnet.uninstallAll');
}
skipInstallCleanupAfterTest = false;
mockState.clear();
MockTelemetryReporter.telemetryEvents = [];
await new FileUtilities().wipeDirectory(storagePath);
Expand Down Expand Up @@ -160,6 +167,96 @@ suite('DotnetCoreAcquisitionExtension End to End', function ()
assert.isTrue(logContents.length > 0, 'Log file is non-empty after activation');
}).timeout(standardTimeoutTime);

test('dotnet.ensureDotnetDependencies prompts when dotnet --info fails with a Linux dependency signal', async () =>
{
const originalPlatform = os.platform;
const originalSpawnSync = cp.spawnSync;
const originalSignalCheck = DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies;
const originalPromptLinuxDependencyInstall = DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall;
let promptCount = 0;

try
{
skipInstallCleanupAfterTest = true;
Object.defineProperty(os, 'platform', { value: () => 'linux', configurable: true, writable: true });
// Stub the platform-gated signal check rather than mutating the read-only process.platform, so this runs on any OS.
DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies = (signal: string) => signal === 'SIGABRT';
Object.defineProperty(cp, 'spawnSync', {
Comment thread
nagilson marked this conversation as resolved.
Comment thread
nagilson marked this conversation as resolved.
configurable: true,
writable: true,
value: (command: string, args?: string[]) =>
{
assert.equal(command, 'dotnet');
assert.deepEqual(args, ['--info']);
return { signal: 'SIGABRT', stderr: Buffer.from('Couldn\'t find a valid ICU package installed on the system.') };
}
});
DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall = async (message: string) =>
{
assert.equal(message, 'Failed to run .NET runtime.');
promptCount++;
return false;
};

await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', { command: 'dotnet', arguments: ['--info'] });

assert.equal(promptCount, 1, 'Missing Linux dependency prompt should be shown when dotnet --info aborts.');
}
finally
{
Object.defineProperty(os, 'platform', { value: originalPlatform, configurable: true, writable: true });
Object.defineProperty(cp, 'spawnSync', { value: originalSpawnSync, configurable: true, writable: true });
DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies = originalSignalCheck;
DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall = originalPromptLinuxDependencyInstall;
}
Comment thread
nagilson marked this conversation as resolved.
}).timeout(standardTimeoutTime);

test('dotnet.ensureDotnetDependencies does not prompt when a dotnet dll payload starts successfully', async () =>
{
const originalPlatform = os.platform;
const originalSpawnSync = cp.spawnSync;
const originalSignalCheck = DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies;
const originalPromptLinuxDependencyInstall = DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall;
let promptCount = 0;

try
{
skipInstallCleanupAfterTest = true;
Object.defineProperty(os, 'platform', { value: () => 'linux', configurable: true, writable: true });
// Stub the platform-gated signal check rather than mutating the read-only process.platform, so this runs on any OS.
DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies = (signal: string) => signal === 'SIGABRT';
Object.defineProperty(cp, 'spawnSync', {
Comment thread
nagilson marked this conversation as resolved.
Comment thread
nagilson marked this conversation as resolved.
configurable: true,
writable: true,
value: (command: string, args?: string[]) =>
{
assert.equal(command, 'dotnet');
assert.deepEqual(args, [path.join('server', 'Microsoft.CodeAnalysis.LanguageServer.dll')]);
return { signal: null };
}
});
DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall = async () =>
{
promptCount++;
return false;
};

await vscode.commands.executeCommand('dotnet.ensureDotnetDependencies', {
command: 'dotnet',
arguments: [path.join('server', 'Microsoft.CodeAnalysis.LanguageServer.dll')]
});

assert.equal(promptCount, 0, 'Missing Linux dependency prompt should not be shown when the dotnet dll payload starts.');
}
finally
{
Object.defineProperty(os, 'platform', { value: originalPlatform, configurable: true, writable: true });
Object.defineProperty(cp, 'spawnSync', { value: originalSpawnSync, configurable: true, writable: true });
DotnetCoreDependencyInstaller.prototype.signalIndicatesMissingLinuxDependencies = originalSignalCheck;
DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall = originalPromptLinuxDependencyInstall;
}
Comment thread
nagilson marked this conversation as resolved.
}).timeout(standardTimeoutTime);

async function installRuntime(dotnetVersion: string, installMode: DotnetInstallMode, arch?: string)
{
let context: IDotnetAcquireContext = { version: dotnetVersion, requestingExtensionId, mode: installMode };
Expand Down
Loading