diff --git a/.gitignore b/.gitignore index ea0c8f290..062afeb3b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ bin node_modules out .omnisharp-*/ +.debugger -install.lock +install.* *.vsix diff --git a/coreclr-debug/.gitignore b/coreclr-debug/.gitignore deleted file mode 100644 index 60944094f..000000000 --- a/coreclr-debug/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -bin -obj -project.lock.json -debugAdapters -install.log -extension.log -project.json diff --git a/coreclr-debug/NuGet.config b/coreclr-debug/NuGet.config deleted file mode 100644 index ef957669a..000000000 --- a/coreclr-debug/NuGet.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/coreclr-debug/dummy.cs b/coreclr-debug/dummy.cs deleted file mode 100644 index 15513fc2c..000000000 --- a/coreclr-debug/dummy.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Dummy -{ - class Dummy - { - static void Main(string[] args) { - // empty boilerplate required by dotnet build/publish to emit an entry point - // The entrypoint created is dummy[.exe], which we rename to OpenDebugAD7[.exe] - // The generated entry point will then run OpenDebugAD7.dll for us - } - } -} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 207d4bc0a..a700a1779 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -14,7 +14,6 @@ const tslint = require('gulp-tslint'); const vsce = require('vsce'); const debugUtil = require('./out/src/coreclr-debug/util'); const debugInstall = require('./out/src/coreclr-debug/install'); -const fs_extra = require('fs-extra-promise'); const packages = require('./out/src/packages'); const logger = require('./out/src/logger'); const platform = require('./out/src/platform'); @@ -26,75 +25,49 @@ const PackageManager = packages.PackageManager; const LinuxDistribution = platform.LinuxDistribution; const PlatformInformation = platform.PlatformInformation; -/// used in offline packaging run so does not clean .vsix -function clean() { - cleanDebugger(); - return cleanOmnisharp(); -} +function cleanSync(deleteVsix) { + del.sync('install.*'); + del.sync('.omnisharp-*'); + del.sync('.debugger'); -gulp.task('clean', ['omnisharp:clean', 'debugger:clean', 'package:clean'], () => { + if (deleteVsix) { + del.sync('*.vsix'); + } +} +gulp.task('clean', () => { + cleanSync(true); }); -/// Omnisharp Tasks -function installOmnisharp(platformInfo, packageJSON) { +// Install Tasks +function install(platformInfo, packageJSON) { const packageManager = new PackageManager(platformInfo, packageJSON); const logger = new Logger(message => process.stdout.write(message)); + const debuggerUtil = new debugUtil.CoreClrDebugUtil(path.resolve('.'), logger); + const debugInstaller = new debugInstall.DebugInstaller(debuggerUtil); return packageManager.DownloadPackages(logger) .then(() => { return packageManager.InstallPackages(logger); + }) + .then(() => { + return util.touchInstallFile(util.InstallFileType.Lock) + }) + .then(() => { + return debugInstaller.finishInstall(); }); } -function cleanOmnisharp() { - return del('.omnisharp-*'); -} - -gulp.task('omnisharp:clean', () => { - return cleanOmnisharp(); -}); +gulp.task('install', ['clean'], () => { + util.setExtensionPath(__dirname); -gulp.task('omnisharp:install', ['omnisharp:clean'], () => { return PlatformInformation.GetCurrent() .then(platformInfo => { - return installOmnisharp(platformInfo, getPackageJSON()); + return install(platformInfo, getPackageJSON()); }); }); -/// Debugger Tasks -function getDebugInstaller() { - return new debugInstall.DebugInstaller(new debugUtil.CoreClrDebugUtil(path.resolve('.')), true); -} - -function installDebugger(runtimeId) { - return getDebugInstaller().install(runtimeId); -} - -function cleanDebugger() { - try { - getDebugInstaller().clean(); - console.log('Cleaned Succesfully'); - } catch (error) { - console.error(error); - } -} - -gulp.task('debugger:install', ['debugger:clean'], () => { - installDebugger(gulp.env.runtimeId) - .then(() => { - console.log('Installed Succesfully'); - }) - .catch((error) => { - console.error(error); - }); -}); - -gulp.task('debugger:clean', () => { - cleanDebugger(); -}); - -/// Packaging Tasks +/// Packaging (VSIX) Tasks function doPackageSync(packageName) { var vsceArgs = []; @@ -113,13 +86,12 @@ function doPackageSync(packageName) { } function doOfflinePackage(platformInfo, packageName, packageJSON) { - return clean() - .then(() => { - return installDebugger(platformInfo.runtimeId); - }) - .then(() => { - return installOmnisharp(platformInfo, packageJSON); - }) + if (process.platform === 'win32') { + throw new Error('Do not build offline packages on windows. Runtime executables will not be marked executable in *nix packages.'); + } + + cleanSync(false); + return install(platformInfo, packageJSON) .then(() => { doPackageSync(packageName + '-' + platformInfo.runtimeId + '.vsix'); }); @@ -130,7 +102,7 @@ function getPackageJSON() { } gulp.task('package:clean', () => { - return del('*.vsix'); + del.sync('*.vsix'); }); gulp.task('package:online', ['clean'], () => { diff --git a/package.json b/package.json index e05690e2c..617d2f882 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ ] }, { - "description": "OmniSharp (.NET Core - OSX / x64)", + "description": "OmniSharp (.NET Core - macOS / x64)", "url": "https://omnisharpdownload.blob.core.windows.net/ext/omnisharp-1.9-beta19-osx-x64-netcoreapp1.0.zip", "installPath": ".omnisharp-coreclr", "runtimeIds": [ @@ -190,6 +190,110 @@ "darwin", "linux" ] + }, + { + "description": ".NET Core Debugger (Windows / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-win7-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "win7-x64" + ] + }, + { + "description": ".NET Core Debugger (macOS / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-osx.10.11-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "osx.10.11-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (CentOS / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-centos.7-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "centos.7-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (Debian / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-debian.8-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "debian.8-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (Fedora / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-fedora.23-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "fedora.23-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (OpenSUSE / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-opensuse.13.2-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "opensuse.13.2-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (RHEL / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-rhel.7.2-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "rhel.7-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (Ubuntu 14 / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-ubuntu.14.04-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "ubuntu.14.04-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] + }, + { + "description": ".NET Core Debugger (Ubuntu 16 / x64)", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-5-0/coreclr-debug-ubuntu.16.04-x64.zip", + "installPath": ".debugger", + "runtimeIds": [ + "ubuntu.16.04-x64" + ], + "binaries": [ + "./OpenDebugAD7", + "./clrdbg" + ] } ], "engines": { diff --git a/src/common.ts b/src/common.ts index 552f63ff7..0ca265554 100644 --- a/src/common.ts +++ b/src/common.ts @@ -60,19 +60,39 @@ export function fileExists(filePath: string): Promise { }); } -function getInstallLockFilePath(): string { - return path.resolve(getExtensionPath(), 'install.lock'); +export enum InstallFileType { + Begin, + Lock } -export function lockFileExists(): Promise { - return fileExists(getInstallLockFilePath()); +function getInstallFilePath(type: InstallFileType): string { + let installFile = 'install.' + InstallFileType[type]; + return path.resolve(getExtensionPath(), installFile); } -export function touchLockFile(): Promise { +export function installFileExists(type: InstallFileType): Promise { + return fileExists(getInstallFilePath(type)); +} + +export function touchInstallFile(type: InstallFileType): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(getInstallFilePath(type), '', err => { + if (err) { + reject(err); + return; + } + + resolve(); + }); + }); +} + +export function deleteInstallFile(type: InstallFileType): Promise { return new Promise((resolve, reject) => { - fs.writeFile(getInstallLockFilePath(), '', err => { + fs.unlink(getInstallFilePath(type), err => { if (err) { - return reject(err); + reject(err); + return; } resolve(); diff --git a/src/coreclr-debug/activate.ts b/src/coreclr-debug/activate.ts index bde6d7227..fc8a828dc 100644 --- a/src/coreclr-debug/activate.ts +++ b/src/coreclr-debug/activate.ts @@ -7,126 +7,50 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import TelemetryReporter from 'vscode-extension-telemetry'; -import { CoreClrDebugUtil } from './util'; +import { CoreClrDebugUtil, DotnetInfo, DotNetCliError } from './util'; import * as debugInstall from './install'; -import { PlatformInformation } from './../platform'; -import * as semver from 'semver'; - -const MINIMUM_SUPPORTED_DOTNET_CLI: string = '1.0.0-preview2-003121'; +import * as path from 'path'; +import { Logger } from './../logger' +let _debugUtil: CoreClrDebugUtil = null; let _reporter: TelemetryReporter = null; -let _channel: vscode.OutputChannel = null; -let _util: CoreClrDebugUtil = null; - -class DotnetInfo -{ - public Version: string; - public OsVersion: string; - public RuntimeId: string; -} +let _logger: Logger = null; -export function activate(context: vscode.ExtensionContext, reporter: TelemetryReporter) { +export function activate(context: vscode.ExtensionContext, reporter: TelemetryReporter, logger: Logger) { + _debugUtil = new CoreClrDebugUtil(context.extensionPath, logger); _reporter = reporter; - _channel = vscode.window.createOutputChannel('coreclr-debug'); - _util = new CoreClrDebugUtil(context.extensionPath, _channel); - - if (CoreClrDebugUtil.existsSync(_util.installCompleteFilePath())) { - console.log('.NET Core Debugger tools already installed'); - return; - } - - checkForDotnetTools().then((dotnetInfo: DotnetInfo) => { - let installer = new debugInstall.DebugInstaller(_util); - _util.createInstallLog(); - - let statusBarMessage = vscode.window.setStatusBarMessage("Downloading and configuring the .NET Core Debugger..."); - - let runtimeId: string = ''; - let installStage: string = "installBegin"; - let installError: string = ''; - let moreErrors: string = ''; - - getPlatformRuntimeId().then(rid => { - runtimeId = rid; - }).then(() => { - return writeInstallBeginFile(); - }).then(() => { - return installer.install(runtimeId); - }).then(() => { - installStage = "completeSuccess"; - 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: dotnetInfo.Version, - osVersion: dotnetInfo.OsVersion, - osRID: dotnetInfo.RuntimeId + _logger = logger; + + if (!CoreClrDebugUtil.existsSync(_debugUtil.debugAdapterDir())) { + // We have been activated but it looks like our package was not installed. This is bad. + logger.appendLine("[ERROR]: C# Extension failed to install the debugger package"); + showInstallErrorMessage(); + } else if (!CoreClrDebugUtil.existsSync(_debugUtil.installCompleteFilePath())) { + _debugUtil.checkDotNetCli() + .then((dotnetInfo: DotnetInfo) => { + 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(); + // TODO: log telemetry? + }); + }, (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? }); - 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 showInstallErrorMessage() { + vscode.window.showErrorMessage("An error occured during installation of the .NET Core Debugger. The C# extension may need to be reinstalled."); } function showDotnetToolsWarning(message: string) : void @@ -153,27 +77,4 @@ function logTelemetry(eventName: string, properties?: {[prop: string]: string}): if (_reporter !== null) { _reporter.sendTelemetryEvent('coreclr-debug/' + eventName, properties); } -} - -function writeInstallBeginFile() : Promise { - return CoreClrDebugUtil.writeEmptyFile(_util.installBeginFilePath()); -} - -function deleteInstallBeginFile() { - if (CoreClrDebugUtil.existsSync(_util.installBeginFilePath())) { - fs.unlinkSync(_util.installBeginFilePath()); - } -} - -function getPlatformRuntimeId(): Promise { - return PlatformInformation.GetCurrent().then(info => { - if (info.runtimeId) { - return info.runtimeId; - } - - // If we got here, this isn't a support runtime ID. - const message = `Unsupported platform: ${info.toString()}` - _util.log(`Error: ${message}`); - throw new Error(message); - }); -} +} \ No newline at end of file diff --git a/src/coreclr-debug/install.ts b/src/coreclr-debug/install.ts index 17eb998dd..c4116f2d2 100644 --- a/src/coreclr-debug/install.ts +++ b/src/coreclr-debug/install.ts @@ -2,12 +2,10 @@ * 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 { CoreClrDebugUtil } from './util'; import * as fs from 'fs'; import * as path from 'path'; -import * as fs_extra from 'fs-extra-promise'; export class InstallError extends Error { public installStage: string; @@ -44,84 +42,30 @@ export class InstallError extends Error { export class DebugInstaller { private _util: CoreClrDebugUtil = null; - private _isOffline; - constructor(util: CoreClrDebugUtil, isOffline?: boolean) { + constructor(util: CoreClrDebugUtil) { this._util = util; - this._isOffline = isOffline || false; } - public install(runtimeId: string): Promise { - let errorBuilder: InstallError = new InstallError(); - errorBuilder.installStage = 'writeProjectJson'; + public finishInstall(): Promise { + let errorBuilder = new InstallError(); - return this.writeProjectJson(runtimeId).then(() => { - errorBuilder.installStage = 'dotnetRestore'; - return this._util.spawnChildProcess('dotnet', ['--verbose', 'restore', '--configfile', 'NuGet.config'], this._util.coreClrDebugDir(), - (data: Buffer) => { - let text: string = data.toString(); - this._util.logRaw(text); - - // Certain errors are only logged to stdout. - // Detect these and make a note of the kind of error. - DebugInstaller.parseRestoreErrors(text, errorBuilder); - }, - (data: Buffer) => { - let text: string = data.toString(); - this._util.logRaw(text); - - // Reference errors are sent to stderr at the end of restore. - DebugInstaller.parseReferenceErrors(text, errorBuilder); - }); - }).then(() => { - errorBuilder.installStage = 'dotnetPublish'; - return this._util.spawnChildProcess('dotnet', ['--verbose', 'publish', '-r', runtimeId, '-o', this._util.debugAdapterDir()], this._util.coreClrDebugDir(), - (data: Buffer) => { - let text: string = data.toString(); - this._util.logRaw(text); - - DebugInstaller.parsePublishErrors(text, errorBuilder); - }); - }).then(() => { - errorBuilder.installStage = 'ensureAd7'; - return this.ensureAd7EngineExists(this._util.debugAdapterDir()); - }).then(() => { - errorBuilder.installStage = 'renameDummyEntrypoint'; - return this.renameDummyEntrypoint(); - }).then(() => { + return Promise.resolve().then(() => { errorBuilder.installStage = 'rewriteManifest'; this.rewriteManifest(); - errorBuilder.installStage = 'writeCompletionFile'; - return this.writeCompletionFile(); - }).catch((e) => { + errorBuilder.installStage = 'writeCompletionFile' + return CoreClrDebugUtil.writeEmptyFile(this._util.installCompleteFilePath()); + }).catch((err) => { if (errorBuilder.errorMessage === null) { // Only give the error message if we don't have any better info, // as this is usually something similar to "Error: 1". - errorBuilder.errorMessage = e; + errorBuilder.errorMessage = err; } throw errorBuilder; }); } - public clean(): void { - let cleanItems: string[] = []; - - cleanItems.push(this._util.debugAdapterDir()); - cleanItems.push(this._util.installLogPath()); - cleanItems.push(path.join(this._util.coreClrDebugDir(), 'bin')); - cleanItems.push(path.join(this._util.coreClrDebugDir(), 'obj')); - cleanItems.push(path.join(this._util.coreClrDebugDir(), 'project.json')); - cleanItems.push(path.join(this._util.coreClrDebugDir(), 'project.lock.json')); - - cleanItems.forEach((item) => { - if (CoreClrDebugUtil.existsSync(item)) { - this._util.log(`Cleaning ${item}`); - fs_extra.removeSync(item); - } - }); - } - private rewriteManifest(): void { const manifestPath = path.join(this._util.extensionDir(), 'package.json'); let manifestString = fs.readFileSync(manifestPath, 'utf8'); @@ -129,7 +73,7 @@ export class DebugInstaller { delete manifestObject.contributes.debuggers[0].runtime; delete manifestObject.contributes.debuggers[0].program; - let programString = './coreclr-debug/debugAdapters/OpenDebugAD7'; + let programString = './.debugger/OpenDebugAD7'; manifestObject.contributes.debuggers[0].windows = { program: programString + '.exe' }; manifestObject.contributes.debuggers[0].osx = { program: programString }; manifestObject.contributes.debuggers[0].linux = { program: programString }; @@ -137,179 +81,4 @@ export class DebugInstaller { manifestString = JSON.stringify(manifestObject, null, 2); fs.writeFileSync(manifestPath, manifestString); } - - private writeCompletionFile(): Promise { - return CoreClrDebugUtil.writeEmptyFile(this._util.installCompleteFilePath()); - } - - private renameDummyEntrypoint(): Promise { - let src = path.join(this._util.debugAdapterDir(), 'dummy'); - let dest = path.join(this._util.debugAdapterDir(), 'OpenDebugAD7'); - - if (!CoreClrDebugUtil.existsSync(src)) { - if (CoreClrDebugUtil.existsSync(src + '.exe')) { - src += '.exe'; - dest += '.exe'; - } - } - - const promise = new Promise((resolve, reject) => { - fs.rename(src, dest, (err) => { - if (err) { - reject(err.code); - } else { - resolve(); - } - }); - }); - - return promise; - } - - private ensureAd7EngineExists(outputDirectory: string): Promise { - let filePath = path.join(outputDirectory, 'coreclr.ad7Engine.json'); - return new Promise((resolve, reject) => { - fs.exists(filePath, (exists) => { - if (exists) { - return resolve(); - } else { - this._util.log(`${filePath} does not exist.`); - this._util.log(''); - // NOTE: The minimum build number is actually less than 1584, but this is the minimum tested build. - this._util.log('Error: The .NET CLI did not correctly restore debugger files. ' + - 'Ensure that you have .NET CLI version 1.0.0 build #001584 or newer. ' + - "You can check your .NET CLI version using 'dotnet --version'."); - return reject('The .NET CLI did not correctly restore debugger files.'); - } - }); - }); - } - - private writeProjectJson(runtimeId: string): Promise { - return new Promise((resolve, reject) => { - const projectJsonPath = path.join(this._util.coreClrDebugDir(), 'project.json'); - this._util.log('Creating ' + projectJsonPath); - - const projectJson = this.createProjectJson(runtimeId); - - fs.writeFile(projectJsonPath, JSON.stringify(projectJson, null, 2), { encoding: 'utf8' }, (err) => { - if (err) { - this._util.log('Error: Unable to write to project.json: ' + err.message); - reject(err.code); - } - else { - resolve(); - } - }); - }); - } - - private createProjectJson(targetRuntime: string): any { - let projectJson = { - name: "dummy", - buildOptions: { - emitEntryPoint: true - }, - dependencies: { - "Microsoft.VisualStudio.clrdbg": "15.0.25626-preview-3219185", - "Microsoft.VisualStudio.clrdbg.MIEngine": "14.0.31028-preview-1", - "Microsoft.VisualStudio.OpenDebugAD7": "1.0.21028-preview-2", - "NETStandard.Library": "1.6.0", - "Newtonsoft.Json": "7.0.1", - "Microsoft.VisualStudio.Debugger.Interop.Portable": "1.0.1", - "System.Collections.Specialized": "4.0.1", - "System.Collections.Immutable": "1.2.0", - "System.Diagnostics.Process": "4.1.0", - "System.Dynamic.Runtime": "4.0.11", - "Microsoft.CSharp": "4.0.1", - "System.Threading.Tasks.Dataflow": "4.6.0", - "System.Threading.Thread": "4.0.0", - "System.Xml.XDocument": "4.0.11", - "System.Xml.XmlDocument": "4.0.1", - "System.Xml.XmlSerializer": "4.0.11", - "System.ComponentModel": "4.0.1", - "System.ComponentModel.Annotations": "4.1.0", - "System.ComponentModel.EventBasedAsync": "4.0.11", - "System.Runtime.Serialization.Primitives": "4.1.1", - "System.Net.Http": "4.1.0" - }, - frameworks: { - "netcoreapp1.0": { - imports: ["dnxcore50", "portable-net45+win8"] - } - }, - runtimes: { - } - }; - - projectJson.runtimes[targetRuntime] = {}; - - if (this._isOffline) { - projectJson.dependencies["Microsoft.NetCore.DotNetHostPolicy"] = "1.0.1"; - } - - return projectJson; - } - - private static parseRestoreErrors(output: string, errorBuilder: InstallError): void { - 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'; - if (line.indexOf(connectionError) !== -1) { - errorBuilder.errorMessage = connectionError; - } - - const parseVersionError: RegExp = /Error reading '.*' at line [0-9]+ column [0-9]+: '.*' is not a valid version string/; - if (parseVersionError.test(line)) { - errorBuilder.errorMessage = 'Invalid version string'; - } - } - }); - } - - private static parseReferenceErrors(output: string, errorBuilder: InstallError): void { - // Reference errors are restated at the end of the output. Find this section first. - let errorRegionRegExp: RegExp = /^Errors in .*project\.json$/gm; - let beginIndex: number = output.search(errorRegionRegExp); - let errorBlock: string = output.slice(beginIndex); - - let lines: string[] = errorBlock.replace(/\r/mg, '').split('\n'); - lines.forEach(line => { - let referenceRegExp: RegExp = /^(?:\t|\ \ \ \ )Unable to resolve '([^']+)'/g; - let match: RegExpMatchArray; - while (match = referenceRegExp.exec(line)) { - let reference: string = match[1]; - if (reference.startsWith('Microsoft') || - reference.startsWith('System') || - reference.startsWith('NETStandard') || - reference.startsWith('Newtonsoft')) { - errorBuilder.errorMessage = `Unable to restore reference '${reference}'`; - } else { - errorBuilder.errorMessage = 'Error(s) encountered restoring private references'; - } - } - }); - } - - private static parsePublishErrors(output: string, errorBuilder: InstallError): void { - let lines: string[] = output.replace(/\r/mg, '').split('\n'); - lines.forEach(line => { - const errorTypeRegExp: RegExp = /^([\w\.]+Exception)/g; - let typeMatch: RegExpMatchArray; - while (typeMatch = errorTypeRegExp.exec(line)) { - let type: string = typeMatch[1]; - if (type === 'System.IO.IOException') { - 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])}'`; - } - } else { - errorBuilder.errorMessage = type; - } - } - }); - } } \ No newline at end of file diff --git a/src/coreclr-debug/proxy.ts b/src/coreclr-debug/proxy.ts index 80bccdeba..2a72701cb 100644 --- a/src/coreclr-debug/proxy.ts +++ b/src/coreclr-debug/proxy.ts @@ -8,6 +8,8 @@ import * as path from 'path'; import { DebugProtocol } from 'vscode-debugprotocol'; import * as child_process from 'child_process'; import { CoreClrDebugUtil } from './util'; +import * as common from './../common'; +import { Logger } from './../logger'; class ProxyErrorResponse implements DebugProtocol.ErrorResponse { public body: { error?: DebugProtocol.Message }; @@ -32,6 +34,18 @@ function serializeProtocolEvent(message: DebugProtocol.ProtocolMessage): string return finalPayload; } +function sendErrorMessage(message: string) { + process.stdout.write(serializeProtocolEvent(new ProxyErrorResponse(message))); +} + +function sendStillDownloadingMessage() { + sendErrorMessage('The .NET Core Debugger is still being downloaded. See the C# Output Window for more information.'); +} + +function sendDownloadingNotStartedMessage() { + sendErrorMessage('Run \'Debug: Download .NET Core Debugger\' in the Command Palette or open a .NET project directory to download the .NET Core Debugger'); +} + // The default extension manifest calls this proxy as the debugger program // When installation of the debugger components finishes, the extension manifest is rewritten so that this proxy is no longer called // If the debugger components have not finished downloading, the proxy displays an error message to the user @@ -39,17 +53,53 @@ function serializeProtocolEvent(message: DebugProtocol.ProtocolMessage): string // This proxy will still be called and launch OpenDebugAD7 as a child process. // During subsequent code sessions, the rewritten manifest will be loaded and this proxy will no longer be called. function proxy() { - let util = new CoreClrDebugUtil(path.resolve(__dirname, '../../../')); + let extensionPath = path.resolve(__dirname, '../../../'); + common.setExtensionPath(extensionPath); + + let logger = new Logger((text) => { console.log(text) }); + let util = new CoreClrDebugUtil(extensionPath, logger); if (!CoreClrDebugUtil.existsSync(util.installCompleteFilePath())) { - if (CoreClrDebugUtil.existsSync(util.installBeginFilePath())) { - process.stdout.write(serializeProtocolEvent(new ProxyErrorResponse('The .NET Core Debugger is still being downloaded. See the Status Bar for more information.'))); - } else { - process.stdout.write(serializeProtocolEvent(new ProxyErrorResponse('Run \'Debug: Download .NET Core Debugger\' in the Command Palette or open a .NET project directory to download the .NET Core Debugger'))); - } + // our install.complete file does not exist yet, meaning we have not rewritten our manifest yet. Try to figure out what if anything the package manager is doing + // the order in which files are dealt with is this: + // 1. install.Begin is created + // 2. install.Lock is created + // 3. install.Begin is deleted + // 4. install.complete is created + + //first check if dotnet is on the path and new enough + util.checkDotNetCli() + .then((dotnetInfo) => { + // next check if we have begun installing packages + common.installFileExists(common.InstallFileType.Begin) + .then((beginExists: boolean) => { + if (beginExists) { + // packages manager has begun + sendStillDownloadingMessage(); + } else { + // begin doesn't exist. There is a chance we finished downloading and begin had been deleted. Check if lock exists + common.installFileExists(common.InstallFileType.Lock) + .then((lockExists) => { + if (lockExists) { + // packages have finished installing but we had not finished rewriting our manifest when F5 came in + sendStillDownloadingMessage(); + } + else { + // no install files existed when we checked. we have likely not been activated + sendDownloadingNotStartedMessage(); + } + }); + } + }); + }, (err) => { + // error from checkDotNetCli + sendErrorMessage(err.ErrorMessage || util.defaultDotNetCliErrorMessage()); + }); } else { + // debugger has finished install and manifest has been rewritten, kick off our debugger process + new Promise((resolve, reject) => { let processPath = path.join(util.debugAdapterDir(), "OpenDebugAD7" + CoreClrDebugUtil.getPlatformExeExtension()); let args = process.argv.slice(2); @@ -70,7 +120,7 @@ function proxy() { process.stdin.setEncoding('utf8'); child.on('error', data => { - util.logToFile(`Child error: ${data}`); + logger.appendLine(`Child error: ${data}`); }); process.on('SIGTERM', () => { @@ -84,11 +134,11 @@ function proxy() { }); process.stdin.on('error', error => { - util.logToFile(`process.stdin error: ${error}`); + logger.appendLine(`process.stdin error: ${error}`); }); process.stdout.on('error', error => { - util.logToFile(`process.stdout error: ${error}`); + logger.appendLine(`process.stdout error: ${error}`); }); child.stdout.on('data', data => { @@ -101,7 +151,7 @@ function proxy() { process.stdin.resume(); }).catch(err => { - util.logToFile(`Promise failed: ${err}`); + logger.appendLine(err); }); } } diff --git a/src/coreclr-debug/util.ts b/src/coreclr-debug/util.ts index d15328ae5..c19b66842 100644 --- a/src/coreclr-debug/util.ts +++ b/src/coreclr-debug/util.ts @@ -8,32 +8,37 @@ import * as child_process from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; -import * as vscode from 'vscode'; +import * as semver from 'semver'; +import { execChildProcess } from './../common' +import { Logger } from './../logger' + +const MINIMUM_SUPPORTED_DOTNET_CLI: string = '1.0.0-preview2-003121'; + +export class DotnetInfo +{ + public Version: string; + public OsVersion: string; + public RuntimeId: string; +} + +export class DotNetCliError extends Error { + public ErrorMessage: string; // the message to display to the user + public ErrorString: string; // the string to log for this error +} export class CoreClrDebugUtil { private _extensionDir: string = ''; - private _coreClrDebugDir: string = ''; private _debugAdapterDir: string = ''; - private _installLogPath: string = ''; - private _installBeginFilePath: string = ''; private _installCompleteFilePath: string = ''; - private _installLog: fs.WriteStream = null; - private _channel: vscode.OutputChannel = null; - - constructor(extensionDir: string, channel?: vscode.OutputChannel) { + constructor(extensionDir: string, logger: Logger) { this._extensionDir = extensionDir; - this._coreClrDebugDir = path.join(this._extensionDir, 'coreclr-debug'); - this._debugAdapterDir = path.join(this._coreClrDebugDir, 'debugAdapters'); - this._installLogPath = path.join(this._coreClrDebugDir, 'install.log'); - this._installBeginFilePath = path.join(this._coreClrDebugDir, 'install.begin'); + this._debugAdapterDir = path.join(this._extensionDir, '.debugger'); this._installCompleteFilePath = path.join(this._debugAdapterDir, 'install.complete'); - - this._channel = channel; } - extensionDir(): string { + public extensionDir(): string { if (this._extensionDir === '') { throw new Error('Failed to set extension directory'); @@ -41,35 +46,14 @@ export class CoreClrDebugUtil return this._extensionDir; } - coreClrDebugDir(): string { - if (this._coreClrDebugDir === '') { - throw new Error('Failed to set coreclrdebug directory'); - } - return this._coreClrDebugDir; - } - - debugAdapterDir(): string { + public debugAdapterDir(): string { if (this._debugAdapterDir === '') { throw new Error('Failed to set debugadpter directory'); } return this._debugAdapterDir; } - installLogPath(): string { - if (this._installLogPath === '') { - throw new Error('Failed to set install log path'); - } - return this._installLogPath; - } - - installBeginFilePath(): string { - if (this._installBeginFilePath === '') { - throw new Error('Failed to set install begin file path'); - } - return this._installBeginFilePath; - } - - installCompleteFilePath(): string { + public installCompleteFilePath(): string { if (this._installCompleteFilePath === '') { throw new Error('Failed to set install complete file path'); @@ -77,41 +61,7 @@ export class CoreClrDebugUtil return this._installCompleteFilePath; } - createInstallLog(): void { - this._installLog = fs.createWriteStream(this.installLogPath()); - } - - closeInstallLog(): void { - if (this._installLog !== null) { - this._installLog.close(); - } - } - - logRaw(message: string): void { - console.log(message); - - if (this._installLog != null) { - this._installLog.write(message); - } - - if (this._channel != null) { - this._channel.append(message); - } - } - - log(message: string): void { - console.log(message); - - if (this._installLog != null) { - this._installLog.write(message); - } - - if (this._channel != null) { - this._channel.appendLine(message); - } - } - - static writeEmptyFile(path: string) : Promise { + public static writeEmptyFile(path: string) : Promise { return new Promise((resolve, reject) => { fs.writeFile(path, '', (err) => { if (err) { @@ -123,39 +73,52 @@ export class CoreClrDebugUtil }); } - public spawnChildProcess(process: string, args: string[], workingDirectory: string, onStdout?: (data: Buffer) => void, onStderr?: (data: Buffer) => void): Promise { - const promise = new Promise((resolve, reject) => { - const child = child_process.spawn(process, args, { cwd: workingDirectory }); - - if (!onStdout) { - onStdout = (data) => { this.logRaw(`${data}`); }; - } - child.stdout.on('data', onStdout); - - if (!onStderr) { - onStderr = (data) => { this.logRaw(`${data}`); }; - } - child.stderr.on('data', onStderr); - - child.on('close', (code: number) => { - if (code != 0) { - this.log(`${process} exited with error code ${code}`);; - reject(new Error(code.toString())); - } - else { - resolve(); + public defaultDotNetCliErrorMessage(): string { + return 'Failed to find up to date dotnet cli on the path.'; + } + + // This function checks for the presence of dotnet on the path and ensures the Version + // is new enough for us. + // Returns: a promise that returns a DotnetInfo class + // Throws: An DotNetCliError() from the return promise if either dotnet does not exist or is too old. + public checkDotNetCli(): Promise + { + let dotnetInfo = new DotnetInfo(); + + return execChildProcess('dotnet --info', process.cwd()) + .then((data: string) => { + let lines: string[] = data.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 dotnetError = new DotNetCliError(); + dotnetError.ErrorMessage = '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.'; + dotnetError.ErrorString = "Failed to spawn 'dotnet --info'"; + throw dotnetError; + }).then(() => { + // succesfully spawned 'dotnet --info', check the Version + if (semver.lt(dotnetInfo.Version, MINIMUM_SUPPORTED_DOTNET_CLI)) + { + let dotnetError = new DotNetCliError(); + dotnetError.ErrorMessage = '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 + '.'; + dotnetError.ErrorString = "dotnet cli is too old"; + throw dotnetError; + } - child.on('error', (error: Error) => { - reject(error); - }); + return dotnetInfo; }); - - return promise; } - static existsSync(path: string) : boolean { + public static existsSync(path: string) : boolean { try { fs.accessSync(path, fs.F_OK); return true; @@ -168,30 +131,11 @@ export class CoreClrDebugUtil } } - static getPlatformExeExtension() : string { + public static getPlatformExeExtension() : string { if (process.platform === 'win32') { return '.exe'; } return ''; } - - static getPlatformLibExtension() : string { - switch (process.platform) { - case 'win32': - return '.dll'; - case 'darwin': - return '.dylib'; - case 'linux': - return '.so'; - default: - throw Error('Unsupported platform ' + process.platform); - } - } - - /** Used for diagnostics only */ - logToFile(message: string): void { - let logFolder = path.resolve(this.coreClrDebugDir(), "extension.log"); - fs.writeFileSync(logFolder, `${message}${os.EOL}`, { flag: 'a' }); - } } diff --git a/src/main.ts b/src/main.ts index 32159f172..e1b14a277 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,6 +15,8 @@ import { Logger } from './logger'; import { PackageManager, Status } from './packages'; import { PlatformInformation } from './platform'; +let _channel: vscode.OutputChannel = null; + export function activate(context: vscode.ExtensionContext): any { const extensionId = 'ms-vscode.csharp'; @@ -25,31 +27,34 @@ export function activate(context: vscode.ExtensionContext): any { util.setExtensionPath(extension.extensionPath); - ensureRuntimeDependencies(extension) + _channel = vscode.window.createOutputChannel('C#'); + + let logger = new Logger(text => _channel.append(text)); + + ensureRuntimeDependencies(extension, logger) .then(() => { // activate language services OmniSharp.activate(context, reporter); // activate coreclr-debug - coreclrdebug.activate(context, reporter); + coreclrdebug.activate(context, reporter, logger); }); } -function ensureRuntimeDependencies(extension: vscode.Extension): Promise { - return util.lockFileExists() +function ensureRuntimeDependencies(extension: vscode.Extension, logger: Logger): Promise { + return util.installFileExists(util.InstallFileType.Lock) .then(exists => { if (!exists) { - return installRuntimeDependencies(extension); + return util.touchInstallFile(util.InstallFileType.Begin).then(() => { + return installRuntimeDependencies(extension, logger); + }); } }); } -function installRuntimeDependencies(extension: vscode.Extension): Promise { - let channel = vscode.window.createOutputChannel('C#'); - channel.show(); - - let logger = new Logger(text => channel.append(text)); - logger.appendLine('Updating C# dependencies...'); +function installRuntimeDependencies(extension: vscode.Extension, logger: Logger): Promise { + logger.append('Updating C# dependencies...'); + _channel.show(); let statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); let status: Status = { @@ -65,10 +70,14 @@ function installRuntimeDependencies(extension: vscode.Extension): Promise { + installationStage = 'getPlatformInfo'; + return PlatformInformation.GetCurrent() + }) .then(info => { platformInfo = info; packageManager = new PackageManager(info, extension.packageJSON); @@ -90,7 +99,7 @@ function installRuntimeDependencies(extension: vscode.Extension): Promise { installationStage = 'touchLockFile'; - return util.touchLockFile(); + return util.touchInstallFile(util.InstallFileType.Lock); }) .catch(error => { errorMessage = error.toString(); @@ -105,6 +114,11 @@ function installRuntimeDependencies(extension: vscode.Extension): Promise { + // 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) => { }); }); }