From 610c50de0592c538b4e2bec94d571c4116c0cf98 Mon Sep 17 00:00:00 2001 From: Gregg Miskelly Date: Fri, 7 Apr 2017 13:39:16 -0700 Subject: [PATCH] Improve dependancy handling / Linux distro handling (#1371) * Improve dependancy handling / Linux distro handling https://github.com/OmniSharp/omnisharp-vscode/issues/1361 https://github.com/OmniSharp/omnisharp-vscode/issues/1323 Changes: 1. Add a setting to control what version of the debugger to use on Linux 2. We no longer automaticially select a debugger on Arch. Instead, we point folks to a web page telling them how to install it. 3. Added logic to the package manager so that it can detect if a package is already installed so it will not be redownloaded. This was needed since I wanted to trigger redownloads in the case that the user added the Linux distro setting. But it seemed like a useful feature anyway for folks on slow internet connections. 4. Moved the install code to its own .ts file * Updates to the changelog * Code review fixes * Restore original whitespace in package.json * Remove 'runtime id' from the install log --- CHANGELOG.md | 3 + package.json | 73 +++++++++++---- src/CSharpExtDownloader.ts | 142 +++++++++++++++++++++++++++++ src/coreclr-debug/activate.ts | 98 +++++++++++--------- src/main.ts | 133 +++------------------------- src/packages.ts | 55 +++++++++++- src/platform.ts | 162 ++++++++++++++++++++-------------- src/vscodePlatform.ts | 41 +++++++++ test/platform.test.ts | 79 ++++++++++++++++- 9 files changed, 536 insertions(+), 250 deletions(-) create mode 100644 src/CSharpExtDownloader.ts create mode 100644 src/vscodePlatform.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 95762e351..db96814b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,16 @@ #### Debugger +* **Arch Linux change**: before, the debugger would automatically use the Ubuntu 16 debugger on Arch. Now we require the debugger to be explicitly set. See https://aka.ms/vscode-csext-arch for more information. * Several bug fixes that addressed problems with launch ([#1318](https://github.com/OmniSharp/omnisharp-vscode/issues/1318), [#1335](https://github.com/OmniSharp/omnisharp-vscode/issues/1335), [#1336](https://github.com/OmniSharp/omnisharp-vscode/issues/1336)) * Fix issue where VS Code would incorrectly display threads as paused ([#1317](https://github.com/OmniSharp/omnisharp-vscode/issues/1317)) +* Added new 'csharp.fallbackDebuggerLinuxRuntimeId' configuration setting to control the version of the debugger used on Linux ([#1361](https://github.com/OmniSharp/omnisharp-vscode/issues/1361)). #### Other Updates and Fixes * Improvements made to project.json package completion experience. ([#1338](https://github.com/OmniSharp/omnisharp-vscode/pull/1338)) * Assets for building and debugging are now always generated with POSIX style paths. ([#1354](https://github.com/OmniSharp/omnisharp-vscode/pull/1354)) +* Improved the extension's runtime dependency download logic to skip re-downloading packages that were already successfully downloaded and installed. ## 1.8.1 (March 31, 2017) diff --git a/package.json b/package.json index 0387954b8..5cb5c7296 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "binaries": [ "./mono.linux-x86", "./run" - ] + ], + "installTestPath": "./bin/mono.linux-x86" }, { "description": "Mono Runtime (Linux / x64)", @@ -94,7 +95,8 @@ "binaries": [ "./mono.linux-x86_64", "./run" - ] + ], + "installTestPath": "./bin/mono.linux-x86_64" }, { "description": "Mono Runtime (macOS)", @@ -106,7 +108,8 @@ "binaries": [ "./mono.osx", "./run" - ] + ], + "installTestPath": "./bin/mono.osx" }, { "description": "Mono Framework Assemblies", @@ -115,7 +118,8 @@ "platforms": [ "darwin", "linux" - ] + ], + "installTestPath": "./bin/framework/mscorlib.dll" }, { "description": "OmniSharp (.NET 4.6 / x86)", @@ -126,7 +130,8 @@ ], "architectures": [ "x86" - ] + ], + "installTestPath": "./bin/omnisharp/OmniSharp.exe" }, { "description": "OmniSharp (.NET 4.6 / x64)", @@ -137,7 +142,8 @@ ], "architectures": [ "x86_64" - ] + ], + "installTestPath": "./bin/omnisharp/OmniSharp.exe" }, { "description": "OmniSharp (Mono 4.6)", @@ -146,7 +152,8 @@ "platforms": [ "darwin", "linux" - ] + ], + "installTestPath": "./bin/omnisharp/OmniSharp.exe" }, { "description": ".NET Core Debugger (Windows / x64)", @@ -155,7 +162,8 @@ "installPath": ".debugger", "runtimeIds": [ "win7-x64" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui.exe" }, { "description": ".NET Core Debugger (macOS / x64)", @@ -168,7 +176,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (CentOS / x64)", @@ -181,7 +190,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Debian / x64)", @@ -194,7 +204,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Fedora 23 / x64)", @@ -207,7 +218,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Fedora 24 / x64)", @@ -220,7 +232,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (OpenSUSE 13 / x64)", @@ -233,7 +246,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (OpenSUSE 42 / x64)", @@ -246,7 +260,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (RHEL / x64)", @@ -259,7 +274,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Ubuntu 14.04 / x64)", @@ -272,7 +288,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Ubuntu 16.04 / x64)", @@ -285,7 +302,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" }, { "description": ".NET Core Debugger (Ubuntu 16.10 / x64)", @@ -298,7 +316,8 @@ "binaries": [ "./vsdbg-ui", "./vsdbg" - ] + ], + "installTestPath": "./.debugger/vsdbg-ui" } ], "engines": { @@ -324,6 +343,22 @@ "default": false, "description": "Suppress the warning that the .NET CLI is not on the path." }, + "csharp.fallbackDebuggerLinuxRuntimeId": { + "type": "string", + "enum": [ + "centos.7-x64", + "debian.8-x64", + "fedora.23-x64", + "fedora.24-x64", + "opensuse.13.2-x64", + "opensuse.42.1-x64", + "rhel.7-x64", + "ubuntu.14.04-x64", + "ubuntu.16.04-x64", + "ubuntu.16.10-x64" + ], + "description": "If the current Linux distribution is not recognized, this option can be used to tell the debugger what version can be used. After changing this option, close VS Code, remove the debugger folder (~/.vscode/extensions/ms-vscode.csharp-/.debugger) if it has already downloaded, and restart VS Code." + }, "csharp.suppressDotnetRestoreNotification": { "type": "boolean", "default": false, diff --git a/src/CSharpExtDownloader.ts b/src/CSharpExtDownloader.ts new file mode 100644 index 000000000..0fee8165f --- /dev/null +++ b/src/CSharpExtDownloader.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import TelemetryReporter from 'vscode-extension-telemetry'; +import * as util from './common'; +import { Logger } from './logger'; +import { PackageManager, Status, PackageError } from './packages'; +import { PlatformInformation } from './platform'; +import { VSCodePlatformInformation } from './vscodePlatform'; + +/* + * Class used to download the runtime dependencies of the C# Extension + */ +export class CSharpExtDownloader +{ + public constructor( + private channel: vscode.OutputChannel, + private logger: Logger, + private reporter: TelemetryReporter /* optional */, + private packageJSON: any) { + } + + public installRuntimeDependencies(): Promise { + this.logger.append('Updating C# dependencies...'); + this.channel.show(); + + let statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); + let status: Status = { + setMessage: text => { + statusItem.text = text; + statusItem.show(); + }, + setDetail: text => { + statusItem.tooltip = text; + statusItem.show(); + } + }; + + // Sends "AcquisitionStart" telemetry to indicate an acquisition started. + if (this.reporter) { + this.reporter.sendTelemetryEvent("AcquisitionStart"); + } + + let platformInfo: PlatformInformation; + let packageManager: PackageManager; + let installationStage = 'touchBeginFile'; + let errorMessage = ''; + let success = false; + + let telemetryProps: any = {}; + + return util.touchInstallFile(util.InstallFileType.Begin) + .then(() => { + installationStage = 'getPlatformInfo'; + return VSCodePlatformInformation.GetCurrent(); + }) + .then(info => { + platformInfo = info; + packageManager = new PackageManager(info, this.packageJSON); + this.logger.appendLine(); + + // Display platform information and RID followed by a blank line + this.logger.appendLine(`Platform: ${info.toString()}`); + this.logger.appendLine(); + + installationStage = 'downloadPackages'; + + const config = vscode.workspace.getConfiguration(); + const proxy = config.get('http.proxy'); + const strictSSL = config.get('http.proxyStrictSSL', true); + + return packageManager.DownloadPackages(this.logger, status, proxy, strictSSL); + }) + .then(() => { + this.logger.appendLine(); + + installationStage = 'installPackages'; + return packageManager.InstallPackages(this.logger, status); + }) + .then(() => { + installationStage = 'touchLockFile'; + return util.touchInstallFile(util.InstallFileType.Lock); + }) + .then(() => { + installationStage = 'completeSuccess'; + success = true; + }) + .catch(error => { + if (error instanceof PackageError) { + // we can log the message in a PackageError to telemetry as we do not put PII in PackageError messages + telemetryProps['error.message'] = error.message; + + if (error.innerError) { + errorMessage = error.innerError.toString(); + } else { + errorMessage = error.message; + } + + if (error.pkg) { + telemetryProps['error.packageUrl'] = error.pkg.url; + } + + } else { + // do not log raw errorMessage in telemetry as it is likely to contain PII. + errorMessage = error.toString(); + } + + this.logger.appendLine(`Failed at stage: ${installationStage}`); + this.logger.appendLine(errorMessage); + }) + .then(() => { + telemetryProps['installStage'] = installationStage; + telemetryProps['platform.architecture'] = platformInfo.architecture; + telemetryProps['platform.platform'] = platformInfo.platform; + telemetryProps['platform.runtimeId'] = platformInfo.runtimeId; + if (platformInfo.distribution) { + telemetryProps['platform.distribution'] = platformInfo.distribution.toString(); + } + + if (this.reporter) { + this.reporter.sendTelemetryEvent('Acquisition', telemetryProps); + } + + this.logger.appendLine(); + installationStage = ''; + this.logger.appendLine('Finished'); + + statusItem.dispose(); + }) + .then(() => { + // We do this step at the end so that we clean up the begin file in the case that we hit above catch block + // Attach a an empty catch to this so that errors here do not propogate + return util.deleteInstallFile(util.InstallFileType.Begin).catch((error) => { }); + }).then(() => { + return success; + }); + + } +} diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts index e897330ba..00bf5441c 100644 --- a/src/coreclr-debug/activate.ts +++ b/src/coreclr-debug/activate.ts @@ -9,29 +9,43 @@ import TelemetryReporter from 'vscode-extension-telemetry'; import { CoreClrDebugUtil, DotnetInfo, } from './util'; import * as debugInstall from './install'; import { Logger } from './../logger'; -import { PlatformInformation } from './../platform'; +import { VSCodePlatformInformation } from './../vscodePlatform'; +import { CSharpExtDownloader } from './../CSharpExtDownloader'; let _debugUtil: CoreClrDebugUtil = null; let _reporter: TelemetryReporter = null; let _logger: Logger = null; -export function activate(context: vscode.ExtensionContext, reporter: TelemetryReporter, logger: Logger, channel: vscode.OutputChannel) { +export function activate(thisExtension : vscode.Extension, context: vscode.ExtensionContext, reporter: TelemetryReporter, logger: Logger, channel: vscode.OutputChannel) { _debugUtil = new CoreClrDebugUtil(context.extensionPath, logger); _reporter = reporter; _logger = logger; if (!CoreClrDebugUtil.existsSync(_debugUtil.debugAdapterDir())) { - PlatformInformation.GetCurrent().then((info) => { + VSCodePlatformInformation.GetCurrent().then((info) => { if (info.runtimeId) { if (info.runtimeId === 'win7-x86') { logger.appendLine(`[WARNING]: x86 Windows is not currently supported by the .NET Core debugger. Debugging will not be available.`); + } else if (info.isLinux() && VSCodePlatformInformation.isFallbackDebuggerLinuxRuntimeIdSet()) { + // The user set the fallback runtime id after the initial extension install, retry again now + const downloader = new CSharpExtDownloader(channel, logger, null, thisExtension.packageJSON); + downloader.installRuntimeDependencies().then((success : boolean) => { + if (success) { + completeDebuggerInstall(logger, channel); + } + }); } else { logger.appendLine("[ERROR]: C# Extension failed to install the debugger package"); showInstallErrorMessage(channel); } } else { if (info.isLinux) { - logger.appendLine(`[WARNING]: The current Linux distribution '${info.distribution.name}' version '${info.distribution.version}' is not currently supported by the .NET Core debugger. Debugging will not be available.`); + if (info.distribution.name === 'arch') { + logger.appendLine("[WARNING]: The .NET Core debugger could not be automatically installed. Follow instructions on https://aka.ms/vscode-csext-arch to enable debugging on Arch Linux."); + } else { + logger.appendLine(`[WARNING]: The current Linux distribution '${info.distribution.name}' version '${info.distribution.version}' is not currently supported by the .NET Core debugger. Debugging will not be available.`); + logger.appendLine(`If '${info.distribution.name}' is binary compatible with a Linux distribution officially supported by .NET Core, you may be able to resolve this by setting 'csharp.fallbackDebuggerLinuxRuntimeId' in 'File->Preferences->Settings' and restarting VS Code.`); + } } else { logger.appendLine(`[WARNING]: The current operating system is not currently supported by the .NET Core debugger. Debugging will not be available.`); } @@ -42,45 +56,49 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe showInstallErrorMessage(channel); }); } else if (!CoreClrDebugUtil.existsSync(_debugUtil.installCompleteFilePath())) { - _debugUtil.checkDotNetCli() - .then((dotnetInfo: DotnetInfo) => { - _debugUtil.checkOpenSSLInstalledIfRequired().then((isInstalled) => { - if (isInstalled) { - let installer = new debugInstall.DebugInstaller(_debugUtil); - installer.finishInstall() - .then(() => { - vscode.window.setStatusBarMessage('Successfully installed .NET Core Debugger.'); - }) - .catch((err) => { - logger.appendLine("[ERROR]: An error occured while installing the .NET Core Debugger:"); - logger.appendLine(err); - showInstallErrorMessage(channel); - // TODO: log telemetry? - }); - } else { - logger.appendLine("[ERROR] The debugger cannot be installed. A required component, OpenSSL, is not correctly configured."); - logger.appendLine("In order to use the debugger, open a terminal window and execute the following instructions."); - logger.appendLine("See https://www.microsoft.com/net/core#macos for more details."); - logger.appendLine(); - logger.appendLine(" brew update"); - logger.appendLine(" brew install openssl"); - logger.appendLine(" mkdir -p /usr/local/lib"); - logger.appendLine(" ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/"); - logger.appendLine(" ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/"); - channel.show(); - vscode.window.showErrorMessage("The .NET Core debugger cannot be installed. OpenSSL is not correctly configured. See the C# output channel for details."); - } - }); - }, (err) => { - // Check for dotnet tools failed. pop the UI - // err is a DotNetCliError but use defaults in the unexpected case that it's not - showDotnetToolsWarning(err.ErrorMessage || _debugUtil.defaultDotNetCliErrorMessage()); - _logger.appendLine(err.ErrorString || err); - // TODO: log telemetry? - }); + completeDebuggerInstall(logger, channel); } } +function completeDebuggerInstall(logger: Logger, channel: vscode.OutputChannel) : void { + _debugUtil.checkDotNetCli() + .then((dotnetInfo: DotnetInfo) => { + _debugUtil.checkOpenSSLInstalledIfRequired().then((isInstalled) => { + if (isInstalled) { + let installer = new debugInstall.DebugInstaller(_debugUtil); + installer.finishInstall() + .then(() => { + vscode.window.setStatusBarMessage('Successfully installed .NET Core Debugger.'); + }) + .catch((err) => { + logger.appendLine("[ERROR]: An error occured while installing the .NET Core Debugger:"); + logger.appendLine(err); + showInstallErrorMessage(channel); + // TODO: log telemetry? + }); + } else { + logger.appendLine("[ERROR] The debugger cannot be installed. A required component, OpenSSL, is not correctly configured."); + logger.appendLine("In order to use the debugger, open a terminal window and execute the following instructions."); + logger.appendLine("See https://www.microsoft.com/net/core#macos for more details."); + logger.appendLine(); + logger.appendLine(" brew update"); + logger.appendLine(" brew install openssl"); + logger.appendLine(" mkdir -p /usr/local/lib"); + logger.appendLine(" ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/"); + logger.appendLine(" ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/"); + channel.show(); + vscode.window.showErrorMessage("The .NET Core debugger cannot be installed. OpenSSL is not correctly configured. See the C# output channel for details."); + } + }); + }, (err) => { + // Check for dotnet tools failed. pop the UI + // err is a DotNetCliError but use defaults in the unexpected case that it's not + showDotnetToolsWarning(err.ErrorMessage || _debugUtil.defaultDotNetCliErrorMessage()); + _logger.appendLine(err.ErrorString || err); + // TODO: log telemetry? + }); +} + function showInstallErrorMessage(channel: vscode.OutputChannel) { channel.show(); vscode.window.showErrorMessage("An error occured during installation of the .NET Core Debugger. The C# extension may need to be reinstalled."); diff --git a/src/main.ts b/src/main.ts index 5d6777782..1e73f241e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,8 +10,7 @@ import * as coreclrdebug from './coreclr-debug/activate'; import * as OmniSharp from './omnisharp/extension'; import * as util from './common'; import { Logger } from './logger'; -import { PackageManager, Status, PackageError } from './packages'; -import { PlatformInformation } from './platform'; +import { CSharpExtDownloader } from './CSharpExtDownloader'; import { addJSONProviders } from './features/json/jsonContributions'; let _channel: vscode.OutputChannel = null; @@ -31,139 +30,29 @@ export function activate(context: vscode.ExtensionContext): any { let logger = new Logger(text => _channel.append(text)); ensureRuntimeDependencies(extension, logger, reporter) - .then(() => { + .then((success : boolean) => { // activate language services OmniSharp.activate(context, reporter); // register JSON completion & hover providers for project.json context.subscriptions.push(addJSONProviders()); - // activate coreclr-debug - coreclrdebug.activate(context, reporter, logger, _channel); + if (success) { + // activate coreclr-debug + coreclrdebug.activate(extension, context, reporter, logger, _channel); + } }); } -function ensureRuntimeDependencies(extension: vscode.Extension, logger: Logger, reporter: TelemetryReporter): Promise { +function ensureRuntimeDependencies(extension: vscode.Extension, logger: Logger, reporter: TelemetryReporter): Promise { return util.installFileExists(util.InstallFileType.Lock) .then(exists => { if (!exists) { - return util.touchInstallFile(util.InstallFileType.Begin).then(() => { - return installRuntimeDependencies(extension, logger, reporter); - }); - } - }); -} - -function installRuntimeDependencies(extension: vscode.Extension, logger: Logger, reporter: TelemetryReporter): Promise { - logger.append('Updating C# dependencies...'); - _channel.show(); - - let statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); - let status: Status = { - setMessage: text => { - statusItem.text = text; - statusItem.show(); - }, - setDetail: text => { - statusItem.tooltip = text; - statusItem.show(); - } - }; - - // Sends "AcquisitionStart" telemetry to indicate an acquisition started. - reporter.sendTelemetryEvent("AcquisitionStart"); - - let platformInfo: PlatformInformation; - let packageManager: PackageManager; - let installationStage = 'touchBeginFile'; - let errorMessage = ''; - - let telemetryProps: any = {}; - - return util.touchInstallFile(util.InstallFileType.Begin) - .then(() => { - installationStage = 'getPlatformInfo'; - return PlatformInformation.GetCurrent(); - }) - .then(info => { - platformInfo = info; - packageManager = new PackageManager(info, extension.packageJSON); - logger.appendLine(); - - // Display platform information and RID followed by a blank line - logger.append(`Platform: ${info.toString()}`); - if (info.runtimeId) { - logger.appendLine(` (${info.runtimeId})`); - } - else { - logger.appendLine(); - } - logger.appendLine(); - - installationStage = 'downloadPackages'; - - const config = vscode.workspace.getConfiguration(); - const proxy = config.get('http.proxy'); - const strictSSL = config.get('http.proxyStrictSSL', true); - - return packageManager.DownloadPackages(logger, status, proxy, strictSSL); - }) - .then(() => { - logger.appendLine(); - - installationStage = 'installPackages'; - return packageManager.InstallPackages(logger, status); - }) - .then(() => { - installationStage = 'touchLockFile'; - return util.touchInstallFile(util.InstallFileType.Lock); - }) - .then(() => { - installationStage = 'completeSuccess'; - }) - .catch(error => { - if (error instanceof PackageError) { - // we can log the message in a PackageError to telemetry as we do not put PII in PackageError messages - telemetryProps['error.message'] = error.message; - - if (error.innerError) { - errorMessage = error.innerError.toString(); - } else { - errorMessage = error.message; - } - - if (error.pkg) { - telemetryProps['error.packageUrl'] = error.pkg.url; - } - + const downloader = new CSharpExtDownloader(_channel, logger, reporter, extension.packageJSON); + return downloader.installRuntimeDependencies(); } else { - // do not log raw errorMessage in telemetry as it is likely to contain PII. - errorMessage = error.toString(); + return true; } - - logger.appendLine(`Failed at stage: ${installationStage}`); - logger.appendLine(errorMessage); - }) - .then(() => { - telemetryProps['installStage'] = installationStage; - telemetryProps['platform.architecture'] = platformInfo.architecture; - telemetryProps['platform.platform'] = platformInfo.platform; - telemetryProps['platform.runtimeId'] = platformInfo.runtimeId; - if (platformInfo.distribution) { - telemetryProps['platform.distribution'] = platformInfo.distribution.toString(); - } - - reporter.sendTelemetryEvent('Acquisition', telemetryProps); - - logger.appendLine(); - installationStage = ''; - logger.appendLine('Finished'); - - statusItem.dispose(); - }) - .then(() => { - // We do this step at the end so that we clean up the begin file in the case that we hit above catch block - // Attach a an empty catch to this so that errors here do not propogate - return util.deleteInstallFile(util.InstallFileType.Begin).catch((error) => { }); }); } + diff --git a/src/packages.ts b/src/packages.ts index b899d3051..38938c265 100644 --- a/src/packages.ts +++ b/src/packages.ts @@ -25,6 +25,9 @@ export interface Package { architectures: string[]; binaries: string[]; tmpFile: tmp.SynchrounousResult; + + // Path to use to test if the package has already been installed + installTestPath?: string; } export interface Status { @@ -55,7 +58,7 @@ export class PackageManager { public DownloadPackages(logger: Logger, status: Status, proxy: string, strictSSL: boolean): Promise { return this.GetPackages() .then(packages => { - return util.buildPromiseChain(packages, pkg => downloadPackage(pkg, logger, status, proxy, strictSSL)); + return util.buildPromiseChain(packages, pkg => maybeDownloadPackage(pkg, logger, status, proxy, strictSSL)); }); } @@ -129,6 +132,16 @@ function getNoopStatus(): Status { }; } +function maybeDownloadPackage(pkg: Package, logger: Logger, status: Status, proxy: string, strictSSL: boolean): Promise { + return doesPackageTestPathExist(pkg).then((exists: boolean) => { + if (!exists) { + return downloadPackage(pkg, logger, status, proxy, strictSSL); + } else { + logger.appendLine(`Skipping package '${pkg.description}' (already downloaded).`); + } + }); +} + function downloadPackage(pkg: Package, logger: Logger, status: Status, proxy: string, strictSSL: boolean): Promise { status = status || getNoopStatus(); @@ -243,6 +256,12 @@ function downloadFile(urlString: string, pkg: Package, logger: Logger, status: S } function installPackage(pkg: Package, logger: Logger, status?: Status): Promise { + + if (!pkg.tmpFile) { + // Download of this package was skipped, so there is nothing to install + return Promise.resolve(); + } + status = status || getNoopStatus(); logger.appendLine(`Installing package '${pkg.description}'`); @@ -250,8 +269,21 @@ function installPackage(pkg: Package, logger: Logger, status?: Status): Promise< status.setMessage("$(desktop-download) Installing packages..."); status.setDetail(`Installing package '${pkg.description}'`); - return new Promise((resolve, reject) => { - if (!pkg.tmpFile || pkg.tmpFile.fd == 0) { + return new Promise((resolve, baseReject) => { + const reject = (err) => { + // If anything goes wrong with unzip, make sure we delete the test path (if there is one) + // so we will retry again later + const testPath = getPackageTestPath(pkg); + if (testPath) { + fs.unlink(testPath, unlinkErr => { + baseReject(err); + }); + } else { + baseReject(err); + } + }; + + if (pkg.tmpFile.fd == 0) { return reject(new PackageError('Downloaded file unavailable', pkg)); } @@ -312,3 +344,20 @@ function installPackage(pkg: Package, logger: Logger, status?: Status): Promise< pkg.tmpFile.removeCallback(); }); } + +function doesPackageTestPathExist(pkg: Package) : Promise { + const testPath = getPackageTestPath(pkg); + if (testPath) { + return util.fileExists(testPath); + } else { + return Promise.resolve(false); + } +} + +function getPackageTestPath(pkg: Package) : string { + if (pkg.installTestPath) { + return path.join(util.getExtensionPath(), pkg.installTestPath); + } else { + return null; + } +} \ No newline at end of file diff --git a/src/platform.ts b/src/platform.ts index d7a774bf4..0f1c5a7b2 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -91,10 +91,11 @@ export class PlatformInformation { public constructor( public platform: string, public architecture: string, - public distribution: LinuxDistribution = null) + public distribution: LinuxDistribution = null, + linuxFallbackRuntimeId: ILinuxRuntimeIdFallback = null) { try { - this.runtimeId = PlatformInformation.getRuntimeId(platform, architecture, distribution); + this.runtimeId = PlatformInformation.getRuntimeId(platform, architecture, distribution, linuxFallbackRuntimeId); } catch (err) { this.runtimeId = null; @@ -135,7 +136,7 @@ export class PlatformInformation { return result; } - public static GetCurrent(): Promise { + public static GetCurrent(linuxFallbackRuntimeId: ILinuxRuntimeIdFallback = null): Promise { let platform = os.platform(); let architecturePromise: Promise; let distributionPromise: Promise; @@ -162,7 +163,7 @@ export class PlatformInformation { return Promise.all([architecturePromise, distributionPromise]) .then(([arch, distro]) => { - return new PlatformInformation(platform, arch, distro); + return new PlatformInformation(platform, arch, distro, linuxFallbackRuntimeId); }); } @@ -192,7 +193,7 @@ export class PlatformInformation { * Returns a supported .NET Core Runtime ID (RID) for the current platform. The list of Runtime IDs * is available at https://github.com/dotnet/corefx/tree/master/pkg/Microsoft.NETCore.Platforms. */ - private static getRuntimeId(platform: string, architecture: string, distribution: LinuxDistribution): string { + private static getRuntimeId(platform: string, architecture: string, distribution: LinuxDistribution, linuxFallbackRuntimeId: ILinuxRuntimeIdFallback): string { // Note: We could do much better here. Currently, we only return a limited number of RIDs that // are officially supported. @@ -216,26 +217,37 @@ export class PlatformInformation { case 'linux': if (architecture === 'x86_64') { - const unknown_distribution = 'unknown_distribution'; - const unknown_version = 'unknown_version'; - // First try the distribution name - let runtimeId = PlatformInformation.getRuntimeIdHelper(distribution.name, distribution.version); + let runtimeId = PlatformInformation.getExactRuntimeId(distribution.name, distribution.version); + + // If we didn't recognize the distribution or version, see if the caller has provided us a fall back value + if ((runtimeId === LinuxRuntimeId.unknown_distribution || runtimeId === LinuxRuntimeId.unknown_version) && linuxFallbackRuntimeId !== null) + { + const fallbackRuntimeValue = linuxFallbackRuntimeId.getFallbackLinuxRuntimeId(); + if (fallbackRuntimeValue) { + runtimeId = fallbackRuntimeValue; + } + } + + // If we don't have a fallback runtime id, try again with more fuzzy matching + if (runtimeId === LinuxRuntimeId.unknown_distribution) { + runtimeId = PlatformInformation.getRuntimeIdHelper(distribution.name, distribution.version); + } // If the distribution isn't one that we understand, but the 'ID_LIKE' field has something that we understand, use that // // NOTE: 'ID_LIKE' doesn't specify the version of the 'like' OS. So we will use the 'VERSION_ID' value. This will restrict // how useful ID_LIKE will be since it requires the version numbers to match up, but it is the best we can do. - if (runtimeId === unknown_distribution && distribution.idLike && distribution.idLike.length > 0) { + if (runtimeId === LinuxRuntimeId.unknown_distribution && distribution.idLike && distribution.idLike.length > 0) { for (let id of distribution.idLike) { runtimeId = PlatformInformation.getRuntimeIdHelper(id, distribution.version); - if (runtimeId !== unknown_distribution) { + if (runtimeId !== LinuxRuntimeId.unknown_distribution) { break; } } } - if (runtimeId !== unknown_distribution && runtimeId !== unknown_version) { + if (runtimeId !== LinuxRuntimeId.unknown_distribution && runtimeId !== LinuxRuntimeId.unknown_version) { return runtimeId; } } @@ -248,98 +260,120 @@ export class PlatformInformation { // Chances are, VS Code doesn't support these platforms either. throw Error('Unsupported platform ' + platform); } - - private static getRuntimeIdHelper(distributionName: string, distributionVersion: string): string { - const unknown_distribution = 'unknown_distribution'; - const unknown_version = 'unknown_version'; - - const centos_7 = 'centos.7-x64'; - const debian_8 = 'debian.8-x64'; - const fedora_23 = 'fedora.23-x64'; - const fedora_24 = 'fedora.24-x64'; - const opensuse_13_2 = 'opensuse.13.2-x64'; - const opensuse_42_1 = 'opensuse.42.1-x64'; - const rhel_7 = 'rhel.7-x64'; - const ubuntu_14_04 = 'ubuntu.14.04-x64'; - const ubuntu_16_04 = 'ubuntu.16.04-x64'; - const ubuntu_16_10 = 'ubuntu.16.10-x64'; + private static getExactRuntimeId(distributionName: string, distributionVersion: string): string { switch (distributionName) { - case 'Zorin OS': - case 'zorin': // ID changed in 12.1 - if (distributionVersion === "12") { - return ubuntu_16_04; - } - break; case 'ubuntu': if (distributionVersion === "14.04") { // This also works for Linux Mint - return ubuntu_14_04; + return LinuxRuntimeId.ubuntu_14_04; } else if (distributionVersion === "16.04") { - return ubuntu_16_04; + return LinuxRuntimeId.ubuntu_16_04; } else if (distributionVersion === "16.10") { - return ubuntu_16_10; + return LinuxRuntimeId.ubuntu_16_10; } - break; - case 'elementary': - case 'elementary OS': - if (distributionVersion.startsWith("0.3")) { - // Elementary OS 0.3 Freya is binary compatible with Ubuntu 14.04 - return ubuntu_14_04; - } - else if (distributionVersion.startsWith("0.4")) { - // Elementary OS 0.4 Loki is binary compatible with Ubuntu 16.04 - return ubuntu_16_04; - } - break; case 'linuxmint': if (distributionVersion.startsWith("18")) { // Linux Mint 18 is binary compatible with Ubuntu 16.04 - return ubuntu_16_04; + return LinuxRuntimeId.ubuntu_16_04; } break; + case 'centos': case 'ol': // Oracle Linux is binary compatible with CentOS - return centos_7; + return LinuxRuntimeId.centos_7; case 'fedora': if (distributionVersion === "23") { - return fedora_23; + return LinuxRuntimeId.fedora_23; } else if (distributionVersion === "24") { - return fedora_24; + return LinuxRuntimeId.fedora_24; } break; case 'opensuse': if (distributionVersion.startsWith("13.")) { - return opensuse_13_2; + return LinuxRuntimeId.opensuse_13_2; } else if (distributionVersion.startsWith("42.")) { - return opensuse_42_1; + return LinuxRuntimeId.opensuse_42_1; } break; case 'rhel': - return rhel_7; + return LinuxRuntimeId.rhel_7; case 'debian': - return debian_8; + return LinuxRuntimeId.debian_8; + default: + return LinuxRuntimeId.unknown_distribution; + } + + return LinuxRuntimeId.unknown_version; + } + + private static getRuntimeIdHelper(distributionName: string, distributionVersion: string): string { + + const runtimeId: string = PlatformInformation.getExactRuntimeId(distributionName, distributionVersion); + if (runtimeId !== LinuxRuntimeId.unknown_distribution) { + return runtimeId; + } + + switch (distributionName) { + case 'Zorin OS': + case 'zorin': // ID changed in 12.1 + if (distributionVersion === "12") { + return LinuxRuntimeId.ubuntu_16_04; + } + break; + + case 'elementary': + case 'elementary OS': + if (distributionVersion.startsWith("0.3")) { + // Elementary OS 0.3 Freya is binary compatible with Ubuntu 14.04 + return LinuxRuntimeId.ubuntu_14_04; + } + else if (distributionVersion.startsWith("0.4")) { + // Elementary OS 0.4 Loki is binary compatible with Ubuntu 16.04 + return LinuxRuntimeId.ubuntu_16_04; + } + break; + case 'galliumos': - if (distributionVersion.startsWith("2.0")) { - return ubuntu_16_04; + if (distributionVersion.startsWith("2.0") || distributionVersion.startsWith("2.1")) { + return LinuxRuntimeId.ubuntu_16_04; } break; - case 'arch': - // NOTE: currently Arch Linux seems to be compatible enough with Ubuntu 16 that this works, - // though in the future this may need to change as Arch follows a rolling release model. - return ubuntu_16_04; + default: - return unknown_distribution; + return LinuxRuntimeId.unknown_distribution; } - return unknown_version; + return LinuxRuntimeId.unknown_version; } } + +class LinuxRuntimeId +{ + public static readonly unknown_distribution = 'unknown_distribution'; + public static readonly unknown_version = 'unknown_version'; + + public static readonly centos_7 = 'centos.7-x64'; + public static readonly debian_8 = 'debian.8-x64'; + public static readonly fedora_23 = 'fedora.23-x64'; + public static readonly fedora_24 = 'fedora.24-x64'; + public static readonly opensuse_13_2 = 'opensuse.13.2-x64'; + public static readonly opensuse_42_1 = 'opensuse.42.1-x64'; + public static readonly rhel_7 = 'rhel.7-x64'; + public static readonly ubuntu_14_04 = 'ubuntu.14.04-x64'; + public static readonly ubuntu_16_04 = 'ubuntu.16.04-x64'; + public static readonly ubuntu_16_10 = 'ubuntu.16.10-x64'; +}; + +export interface ILinuxRuntimeIdFallback +{ + getFallbackLinuxRuntimeId() : string; +} \ No newline at end of file diff --git a/src/vscodePlatform.ts b/src/vscodePlatform.ts new file mode 100644 index 000000000..762eb02aa --- /dev/null +++ b/src/vscodePlatform.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import { PlatformInformation, ILinuxRuntimeIdFallback } from './platform'; + +/* + * extension to the PlatformInformation that calls VS Code APIs in order to obtain the runtime id + * for distributions that the extension doesn't understand + */ +export class VSCodePlatformInformation +{ + public static GetCurrent(): Promise { + + class VSCodeLinuxRuntimeIdFallback implements ILinuxRuntimeIdFallback { + getFallbackLinuxRuntimeId(): string { + return VSCodePlatformInformation.fallbackDebuggerLinuxRuntimeId(); + } + }; + + return PlatformInformation.GetCurrent(new VSCodeLinuxRuntimeIdFallback()); + } + + public static isFallbackDebuggerLinuxRuntimeIdSet() : boolean { + if (VSCodePlatformInformation.fallbackDebuggerLinuxRuntimeId()) { + return true; + } + + return false; + } + + private static fallbackDebuggerLinuxRuntimeId() : string { + const config = vscode.workspace.getConfiguration('csharp'); + return config.get('fallbackDebuggerLinuxRuntimeId', ''); + } +}; + diff --git a/test/platform.test.ts b/test/platform.test.ts index fe815c667..738fd5573 100644 --- a/test/platform.test.ts +++ b/test/platform.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { should } from 'chai'; -import { LinuxDistribution, PlatformInformation } from '../src/platform'; +import { LinuxDistribution, PlatformInformation, ILinuxRuntimeIdFallback } from '../src/platform'; suite("Platform", () => { suiteSetup(() => should()); @@ -132,6 +132,45 @@ suite("Platform", () => { should().equal(platformInfo.runtimeId, null); }) + + test("Compute correct RID for GalliumOS 2.0", () => { + const platformInfo = new PlatformInformation('linux', 'x86_64', distro_gallium_2_0()); + + platformInfo.runtimeId.should.equal('ubuntu.16.04-x64'); + }) + + test("Fallback runtime id not used for Ubuntu", () => { + const linuxFallbackRuntimeId = new TestLinuxRuntimeIdFallback(''); + const platformInfo = new PlatformInformation('linux', 'x86_64', distro_ubuntu_16_04(), linuxFallbackRuntimeId); + + platformInfo.runtimeId.should.equal('ubuntu.16.04-x64'); + linuxFallbackRuntimeId.wasFallbackQueried().should.equal(false); + }) + + test("Fallback runtime id used for unknown distro", () => { + const linuxFallbackRuntimeId = new TestLinuxRuntimeIdFallback('ubuntu.16.04-x64'); + const platformInfo = new PlatformInformation('linux', 'x86_64', distro_unknown_no_id_like(), linuxFallbackRuntimeId); + + platformInfo.runtimeId.should.equal('ubuntu.16.04-x64'); + linuxFallbackRuntimeId.wasFallbackQueried().should.equal(true); + }) + + test("Fallback runtime id ignored when not provided", () => { + const linuxFallbackRuntimeId = new TestLinuxRuntimeIdFallback(''); + const platformInfo = new PlatformInformation('linux', 'x86_64', distro_unknown_no_id_like(), linuxFallbackRuntimeId); + + should().equal(platformInfo.runtimeId, null); + linuxFallbackRuntimeId.wasFallbackQueried().should.equal(true); + }) + + test("Fallback runtime id precedence over heuristics", () => { + // NOTE: gallium normally uses ubuntu 16. In this test we force it to instead be something else. + const linuxFallbackRuntimeId = new TestLinuxRuntimeIdFallback('fedora.23-x64'); + const platformInfo = new PlatformInformation('linux', 'x86_64', distro_gallium_2_0(), linuxFallbackRuntimeId); + + platformInfo.runtimeId.should.equal('fedora.23-x64'); + linuxFallbackRuntimeId.wasFallbackQueried().should.equal(true); + }) }); function distro_ubuntu_14_04(): LinuxDistribution { @@ -306,4 +345,40 @@ VERSION="1.0 (rogers)" ID=MakeBelieve`; return LinuxDistribution.FromReleaseInfo(input, '\n'); -} \ No newline at end of file +} + +function distro_gallium_2_0(): LinuxDistribution { + const input = ` +NAME="GalliumOS" +VERSION="2.0 (Xenon)" +ID=galliumos +ID_LIKE="ubuntu debian" +PRETTY_NAME="GalliumOS 2.0" +ANSI_COLOR="1;34" +VERSION_ID="2.0" +HOME_URL="https://galliumos.org/" +SUPPORT_URL="https://galliumos.org/" +BUG_REPORT_URL="https://github.com/GalliumOS/galliumos-distro/issues" +UBUNTU_CODENAME=xenial`; + + return LinuxDistribution.FromReleaseInfo(input, '\n'); +} + +class TestLinuxRuntimeIdFallback implements ILinuxRuntimeIdFallback { + private _fallbackRuntime: string; + private _wasFallbackQueried: boolean; + + constructor(fallbackRuntime: string) { + this._fallbackRuntime = fallbackRuntime; + this._wasFallbackQueried = false; + } + + getFallbackLinuxRuntimeId(): string { + this._wasFallbackQueried = true; + return this._fallbackRuntime; + } + + wasFallbackQueried(): boolean { + return this._wasFallbackQueried; + } +};