diff --git a/package.json b/package.json index 678917d9db..7b1a7f2429 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "csharp", "publisher": "ms-vscode", - "version": "1.4.0-beta7", + "version": "1.4.0-beta8", "description": "C# for Visual Studio Code (powered by OmniSharp).", "displayName": "C#", "author": "Microsoft Corporation", diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts index 0b76c18886..97e4d900a3 100644 --- a/src/coreclr-debug/activate.ts +++ b/src/coreclr-debug/activate.ts @@ -5,18 +5,26 @@ 'use strict'; import * as vscode from 'vscode'; -import * as child_process from 'child_process'; import * as fs from 'fs'; -import * as path from 'path'; import TelemetryReporter from 'vscode-extension-telemetry'; import { CoreClrDebugUtil } from './util'; import * as debugInstall from './install'; import { Platform, getCurrentPlatform } from './../platform'; +import * as semver from 'semver'; + +const MINIMUM_SUPPORTED_DOTNET_CLI: string = '1.0.0-preview2-003121'; let _reporter: TelemetryReporter = null; let _channel: vscode.OutputChannel = null; let _util: CoreClrDebugUtil = null; +class DotnetInfo +{ + public Version: string; + public OsVersion: string; + public RuntimeId: string; +} + export function activate(context: vscode.ExtensionContext, reporter: TelemetryReporter) { _reporter = reporter; _channel = vscode.window.createOutputChannel('coreclr-debug'); @@ -27,22 +35,7 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe return; } - let dotnetVersion: string = ''; - let osVersion: string = ''; - let osRID: string = ''; - _util.spawnChildProcess('dotnet', ['--info'], _util.coreClrDebugDir(), (data: Buffer) => { - var lines: string[] = data.toString().replace(/\r/mg, '').split('\n'); - lines.forEach(line => { - let match: RegExpMatchArray; - if (match = /^\ Version:\s*([^\s].*)$/.exec(line)) { - dotnetVersion = match[1]; - } else if (match = /^\ OS Version:\s*([^\s].*)$/.exec(line)) { - osVersion = match[1]; - } else if (match = /^\ RID:\s*([\w\-\.]+)$/.exec(line)) { - osRID = match[1]; - } - }); - }).then(() => { + checkForDotnetTools().then((dotnetInfo: DotnetInfo) => { let installer = new debugInstall.DebugInstaller(_util); _util.createInstallLog(); @@ -61,54 +54,98 @@ export function activate(context: vscode.ExtensionContext, reporter: TelemetryRe statusBarMessage.dispose(); vscode.window.setStatusBarMessage('Successfully installed .NET Core Debugger.'); }).catch((error: debugInstall.InstallError) => { - const viewLogMessage = "View Log"; - vscode.window.showErrorMessage('Error while installing .NET Core Debugger.', viewLogMessage).then(value => { - if (value === viewLogMessage) { - _channel.show(vscode.ViewColumn.Three); - } - }); - statusBarMessage.dispose(); - - installStage = error.installStage; - installError = error.errorMessage; - moreErrors = error.hasMoreErrors ? 'true' : 'false'; - }).then(() => { - // log telemetry and delete install begin file - logTelemetry('Acquisition', { - installStage: installStage, - installError: installError, - moreErrors: moreErrors, - dotnetVersion: dotnetVersion, - osVersion: osVersion, - osRID: osRID - }); - try { - deleteInstallBeginFile(); - } catch (err) { - // if this throws there's really nothing we can do + const viewLogMessage = "View Log"; + vscode.window.showErrorMessage('Error while installing .NET Core Debugger.', viewLogMessage).then(value => { + if (value === viewLogMessage) { + _channel.show(vscode.ViewColumn.Three); } - _util.closeInstallLog(); }); - }).catch(() => { - const config = vscode.workspace.getConfiguration('csharp'); - if (!config.get('suppressDotnetInstallWarning', false)) { - const getDotNetMessage = 'Get .NET CLI tools'; - const goToSettingsMessage = 'Disable this message in user settings'; - // Buttons are shown in right-to-left order, with a close button to the right of everything; - // getDotNetMessage will be the first button, then goToSettingsMessage, then the close button. - vscode.window.showErrorMessage('The .NET CLI tools cannot be located. .NET Core debugging will not be enabled. Make sure .NET CLI tools are installed and are on the path.', - goToSettingsMessage, getDotNetMessage).then(value => { - if (value === getDotNetMessage) { - let open = require('open'); - open('https://www.microsoft.com/net/core'); - } else if (value === goToSettingsMessage) { - vscode.commands.executeCommand('workbench.action.openGlobalSettings'); - } - }); + statusBarMessage.dispose(); + + installStage = error.installStage; + installError = error.errorMessage; + moreErrors = error.hasMoreErrors ? 'true' : 'false'; + }).then(() => { + // log telemetry and delete install begin file + logTelemetry('Acquisition', { + installStage: installStage, + installError: installError, + moreErrors: moreErrors, + dotnetVersion: dotnetInfo.Version, + osVersion: dotnetInfo.OsVersion, + osRID: dotnetInfo.RuntimeId + }); + try { + deleteInstallBeginFile(); + } catch (err) { + // if this throws there's really nothing we can do + } + _util.closeInstallLog(); + }); + }).catch((error) => { + // log errors from checkForDotnetTools + _util.log(error.message); + }); +} + +// This function checks for the presence of dotnet on the path and ensures the Version +// is new enough for us. Any error UI that needs to be displayed is handled by this function. +// Returns: a promise that returns a DotnetInfo class +// Throws: An Error() from the return promise if either dotnet does not exist or is too old. +function checkForDotnetTools() : Promise +{ + let dotnetInfo = new DotnetInfo(); + + return _util.spawnChildProcess('dotnet', ['--info'], _util.coreClrDebugDir(), (data: Buffer) => { + let lines: string[] = data.toString().replace(/\r/mg, '').split('\n'); + lines.forEach(line => { + let match: RegExpMatchArray; + if (match = /^\ Version:\s*([^\s].*)$/.exec(line)) { + dotnetInfo.Version = match[1]; + } else if (match = /^\ OS Version:\s*([^\s].*)$/.exec(line)) { + dotnetInfo.OsVersion = match[1]; + } else if (match = /^\ RID:\s*([\w\-\.]+)$/.exec(line)) { + dotnetInfo.RuntimeId = match[1]; + } + }); + }).catch((error) => { + // something went wrong with spawning 'dotnet --info' + let message = 'The .NET CLI tools cannot be located. .NET Core debugging will not be enabled. Make sure .NET CLI tools are installed and are on the path.'; + showDotnetToolsWarning(message); + throw new Error("Failed to spawn 'dotnet --info'"); + }).then(() => { + // succesfully spawned 'dotnet --info', check the Version + if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) + { + let message = 'The .NET CLI tools on the path are too old. .NET Core debugging will not be enabled. The minimum supported version is ' + MINIMUM_SUPPORTED_DOTNET_CLI + '.'; + showDotnetToolsWarning(message); + throw new Error("dotnet cli is too old"); } + + return dotnetInfo; }); } +function showDotnetToolsWarning(message: string) : void +{ + const config = vscode.workspace.getConfiguration('csharp'); + if (!config.get('suppressDotnetInstallWarning', false)) { + const getDotNetMessage = 'Get .NET CLI tools'; + const goToSettingsMessage = 'Disable this message in user settings'; + // Buttons are shown in right-to-left order, with a close button to the right of everything; + // getDotNetMessage will be the first button, then goToSettingsMessage, then the close button. + vscode.window.showErrorMessage(message, + goToSettingsMessage, getDotNetMessage).then(value => { + if (value === getDotNetMessage) { + let open = require('open'); + open('https://www.microsoft.com/net/core'); + } else if (value === goToSettingsMessage) { + vscode.commands.executeCommand('workbench.action.openGlobalSettings'); + } + }); + } +} + function logTelemetry(eventName: string, properties?: {[prop: string]: string}): void { if (_reporter !== null) { _reporter.sendTelemetryEvent('coreclr-debug/' + eventName, properties); diff --git a/src/coreclr-debug/install.ts b/src/coreclr-debug/install.ts index 07b978cc16..2b9ab59bed 100644 --- a/src/coreclr-debug/install.ts +++ b/src/coreclr-debug/install.ts @@ -7,7 +7,6 @@ import { CoreClrDebugUtil } from './util'; import * as fs from 'fs'; import * as path from 'path'; -import * as child_process from 'child_process'; import * as fs_extra from 'fs-extra-promise'; export class InstallError extends Error { @@ -53,14 +52,14 @@ export class DebugInstaller { } public install(runtimeId: string): Promise { - var errorBuilder: InstallError = new InstallError(); + let errorBuilder: InstallError = new InstallError(); errorBuilder.installStage = 'writeProjectJson'; return this.writeProjectJson(runtimeId).then(() => { errorBuilder.installStage = 'dotnetRestore'; return this._util.spawnChildProcess('dotnet', ['--verbose', 'restore', '--configfile', 'NuGet.config'], this._util.coreClrDebugDir(), (data: Buffer) => { - var text: string = data.toString(); + let text: string = data.toString(); this._util.logRaw(text); // Certain errors are only logged to stdout. @@ -68,7 +67,7 @@ export class DebugInstaller { DebugInstaller.parseRestoreErrors(text, errorBuilder); }, (data: Buffer) => { - var text: string = data.toString(); + let text: string = data.toString(); this._util.logRaw(text); // Reference errors are sent to stderr at the end of restore. @@ -78,7 +77,7 @@ export class DebugInstaller { errorBuilder.installStage = 'dotnetPublish'; return this._util.spawnChildProcess('dotnet', ['--verbose', 'publish', '-r', runtimeId, '-o', this._util.debugAdapterDir()], this._util.coreClrDebugDir(), (data: Buffer) => { - var text: string = data.toString(); + let text: string = data.toString(); this._util.logRaw(text); DebugInstaller.parsePublishErrors(text, errorBuilder); @@ -253,7 +252,7 @@ export class DebugInstaller { } private static parseRestoreErrors(output: string, errorBuilder: InstallError): void { - var lines: string[] = output.replace(/\r/mg, '').split('\n'); + let lines: string[] = output.replace(/\r/mg, '').split('\n'); lines.forEach(line => { if (line.startsWith('error')) { const connectionError: string = 'The server name or address could not be resolved'; @@ -271,16 +270,16 @@ export class DebugInstaller { private static parseReferenceErrors(output: string, errorBuilder: InstallError): void { // Reference errors are restated at the end of the output. Find this section first. - var errorRegionRegExp: RegExp = /^Errors in .*project\.json$/gm - var beginIndex: number = output.search(errorRegionRegExp); - var errorBlock: string = output.slice(beginIndex); + let errorRegionRegExp: RegExp = /^Errors in .*project\.json$/gm; + let beginIndex: number = output.search(errorRegionRegExp); + let errorBlock: string = output.slice(beginIndex); - var lines: string[] = errorBlock.replace(/\r/mg, '').split('\n'); + let lines: string[] = errorBlock.replace(/\r/mg, '').split('\n'); lines.forEach(line => { - var referenceRegExp: RegExp = /^(?:\t|\ \ \ \ )Unable to resolve '([^']+)'/g - var match: RegExpMatchArray; + let referenceRegExp: RegExp = /^(?:\t|\ \ \ \ )Unable to resolve '([^']+)'/g; + let match: RegExpMatchArray; while (match = referenceRegExp.exec(line)) { - var reference: string = match[1]; + let reference: string = match[1]; if (reference.startsWith('Microsoft') || reference.startsWith('System') || reference.startsWith('NETStandard') || @@ -294,15 +293,15 @@ export class DebugInstaller { } private static parsePublishErrors(output: string, errorBuilder: InstallError): void { - var lines: string[] = output.replace(/\r/mg, '').split('\n'); + let lines: string[] = output.replace(/\r/mg, '').split('\n'); lines.forEach(line => { - var errorTypeRegExp: RegExp = /^([\w\.]+Exception)/g - var typeMatch: RegExpMatchArray; + const errorTypeRegExp: RegExp = /^([\w\.]+Exception)/g; + let typeMatch: RegExpMatchArray; while (typeMatch = errorTypeRegExp.exec(line)) { - var type: string = typeMatch[1]; + let type: string = typeMatch[1]; if (type === 'System.IO.IOException') { - var ioExceptionRegExp: RegExp = /System\.IO\.IOException: The process cannot access the file '(.*)' because it is being used by another process./g - var ioMatch: RegExpMatchArray; + const ioExceptionRegExp: RegExp = /System\.IO\.IOException: The process cannot access the file '(.*)' because it is being used by another process./g; + let ioMatch: RegExpMatchArray; if (ioMatch = ioExceptionRegExp.exec(line)) { // Remove path as it may contain user information. errorBuilder.errorMessage = `System.IO.IOException: unable to access '${path.basename(ioMatch[1])}'`; diff --git a/src/coreclr-debug/util.ts b/src/coreclr-debug/util.ts index a0470f5d2c..d15328ae5f 100644 --- a/src/coreclr-debug/util.ts +++ b/src/coreclr-debug/util.ts @@ -149,7 +149,7 @@ export class CoreClrDebugUtil child.on('error', (error: Error) => { reject(error); - }) + }); }); return promise;