Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 6 additions & 0 deletions Documentation/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ 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.

The TypeScript type for `arguments` includes both `string[]` and `child_process.SpawnSyncOptionsWithStringEncoding`. The `string[]` member reflects the runtime behavior that existing callers already use today, so adding it to the published type is not a breaking change. The older options-object shape remains accepted for compatibility with the previously published definition.
Comment thread
nagilson marked this conversation as resolved.
Outdated

Comment thread
nagilson marked this conversation as resolved.
Outdated
### 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
4 changes: 3 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,9 @@ ${JSON.stringify(commandContext)}`));
return;
}

const result = cp.spawnSync(commandContext.command, commandContext.arguments);
const result = Array.isArray(commandContext.arguments)
? cp.spawnSync(commandContext.command, commandContext.arguments)
: cp.spawnSync(commandContext.command, commandContext.arguments);
Comment thread
nagilson marked this conversation as resolved.
Outdated
Comment thread
nagilson marked this conversation as resolved.
Outdated
Comment thread
Copilot marked this conversation as resolved.
Outdated
const installer = new DotnetCoreDependencyInstaller();
if (installer.signalIndicatesMissingLinuxDependencies(result.signal!))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* 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';
Expand All @@ -13,6 +14,7 @@ import
DotnetInstallMode,
DotnetInstallType,
DotnetVersionSpecRequirement,
DotnetCoreDependencyInstaller,
EnvironmentVariableIsDefined,
FileUtilities,
getDistroInfo,
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,94 @@ 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 originalProcessPlatform = process.platform;
const originalSpawnSync = cp.spawnSync;
const originalPromptLinuxDependencyInstall = DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall;
let promptCount = 0;

try
{
skipInstallCleanupAfterTest = true;
Object.defineProperty(os, 'platform', { value: () => 'linux', configurable: true, writable: true });
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true, writable: true });
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(process, 'platform', { value: originalProcessPlatform, configurable: true, writable: true });
Object.defineProperty(cp, 'spawnSync', { value: originalSpawnSync, configurable: true, writable: true });
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 originalProcessPlatform = process.platform;
const originalSpawnSync = cp.spawnSync;
const originalPromptLinuxDependencyInstall = DotnetCoreDependencyInstaller.prototype.promptLinuxDependencyInstall;
let promptCount = 0;

try
{
skipInstallCleanupAfterTest = true;
Object.defineProperty(os, 'platform', { value: () => 'linux', configurable: true, writable: true });
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true, writable: true });
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(process, 'platform', { value: originalProcessPlatform, configurable: true, writable: true });
Object.defineProperty(cp, 'spawnSync', { value: originalSpawnSync, configurable: true, writable: true });
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
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ checkNetCoreDeps(){
}

checkAdditionalDeps(){
if [ "$ADDITIONAL_DEPS" -ne "" ]; then
if [ "$ADDITIONAL_DEPS" != "" ]; then
# Install additional dependencies
if ! "$1" "$2 $ADDITIONAL_DEPS"; then
echo "(!) Failed to install additional dependencies!"
Expand Down Expand Up @@ -125,7 +125,7 @@ fi
#openSUSE - Has to be first since apt-get is available but package names different
if [ "$DISTRO" = "SUSE" ]; then
echo "(*) Detected SUSE (unoffically/community supported)"
Comment thread
nagilson marked this conversation as resolved.
installAdditionalDeps sudoIf "zypper -n in"
checkAdditionalDeps sudoIf "zypper -n in"
checkNetCoreDeps sudoIf "zypper -n in libopenssl1_0_0 libicu krb5 libz1"

# Debian / Ubuntu
Expand All @@ -139,8 +139,8 @@ elif [ "$DISTRO" = "Debian" ]; then
exitScript 1
fi

installAdditionalDeps aptSudoIf "install -yq"
checkNetCoreDeps aptSudoIf "install -yq libicu[0-9][0-9] libkrb5-3 zlib1g $ADDITIONAL_DEPS"
checkAdditionalDeps aptSudoIf "install -yq"
checkNetCoreDeps aptSudoIf "install -yq ^libicu[0-9][0-9]*$ libkrb5-3 zlib1g $ADDITIONAL_DEPS"
Comment thread
nagilson marked this conversation as resolved.
Outdated
if [ $SKIPDOTNETCORE -eq 0 ]; then
# Determine which version of libssl to install
# dpkg-query can return "1" in some distros if the package is not found. "2" is an unexpected error
Expand Down Expand Up @@ -180,7 +180,7 @@ elif [ "$DISTRO" = "RedHat" ]; then
exitScript 1
fi

installAdditionalDeps sudoIf "yum -y install"
checkAdditionalDeps sudoIf "yum -y install"
checkNetCoreDeps sudoIf "yum -y install openssl-libs krb5-libs libicu zlib"
# Install openssl-compat10 for Fedora 29. Does not exist in
# CentOS, so validate package exists first.
Expand All @@ -198,13 +198,13 @@ elif [ "$DISTRO" = "RedHat" ]; then
#ArchLinux
elif [ "$DISTRO" = "ArchLinux" ]; then
echo "(*) Detected Arch Linux (unoffically/community supported)"
Comment thread
nagilson marked this conversation as resolved.
installAdditionalDeps sudoIf "pacman -Sq --noconfirm --needed"
checkAdditionalDeps sudoIf "pacman -Sq --noconfirm --needed"
checkNetCoreDeps sudoIf "pacman -Sq --noconfirm --needed gcr liburcu openssl-1.0 krb5 icu zlib"

#Solus
elif [ "$DISTRO" = "Solus" ]; then
echo "(*) Detected Solus (unoffically/community supported)"
Comment thread
nagilson marked this conversation as resolved.
installAdditionalDeps sudoIf "eopkg -y it"
checkAdditionalDeps sudoIf "eopkg -y it"
checkNetCoreDeps sudoIf "eopkg -y it libicu openssl zlib kerberos"

#Alpine Linux
Expand All @@ -223,7 +223,7 @@ elif [ "$DISTRO" = "Alpine" ]; then
exitScript 1
fi

installAdditionalDeps sudoIf "apk add --no-cache"
checkAdditionalDeps sudoIf "apk add --no-cache"
sudoIf "apk add --no-cache libssl1.0 icu krb5 zlib"

# Unknown distro
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ import { EnsureDependenciesErrorConfiguration } from './Utils/ErrorHandler';

export interface IDotnetEnsureDependenciesContext {
command: string;
arguments: cp.SpawnSyncOptionsWithStringEncoding;
arguments: string[] | cp.SpawnSyncOptionsWithStringEncoding;
errorConfiguration?: EnsureDependenciesErrorConfiguration;
}
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 chai from 'chai';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

const assert = chai.assert;

function writeExecutable(filePath: string, content: string): void
{
fs.writeFileSync(filePath, content);
fs.chmodSync(filePath, 0o755);
}

suite('Linux Prereqs Installer Script Unit Tests', function ()
{
test('Debian install uses a libicu package pattern that supports newer package versions', function ()
{
if (os.platform() !== 'linux')
{
this.skip();
}

const scriptPath = path.resolve(__dirname, '../../../install scripts/install-linux-prereqs.sh');
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'dotnet-prereqs-script-'));
const fakeBin = path.join(testRoot, 'bin');
const aptGetLog = path.join(testRoot, 'apt-get.log');
fs.mkdirSync(fakeBin);

try
{
writeExecutable(path.join(fakeBin, 'id'), '#!/usr/bin/env bash\nif [ "$1" = "-u" ]; then echo 0; exit 0; fi\nexit 0\n');
writeExecutable(path.join(fakeBin, 'fuser'), '#!/usr/bin/env bash\nexit 1\n');
writeExecutable(path.join(fakeBin, 'apt-get'), '#!/usr/bin/env bash\necho "$*" >> "$APT_GET_LOG"\nexit 0\n');
writeExecutable(path.join(fakeBin, 'dpkg-query'), '#!/usr/bin/env bash\nprintf "ii\\tlibssl1.0.0:amd64\\n"\nexit 0\n');

const result = cp.spawnSync('bash', [scriptPath, 'Debian', '', 'false', ''], {
encoding: 'utf8',
env: {
...process.env,
APT_GET_LOG: aptGetLog,
PATH: `${fakeBin}:${process.env.PATH ?? ''}`
}
});

assert.equal(result.status, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
assert.notInclude(result.stderr, 'command not found');
assert.notInclude(result.stderr, 'integer expected');

const aptGetCalls = fs.readFileSync(aptGetLog, 'utf8');
assert.include(aptGetCalls, 'update');
assert.include(aptGetCalls, 'install -yq ^libicu[0-9][0-9]*$ libkrb5-3 zlib1g');
}
finally
{
fs.rmSync(testRoot, { recursive: true, force: true });
}
});
});