diff --git a/.gitignore b/.gitignore index 5bf6ff89bf..10415c3b8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ bin node_modules out + +*.vsix diff --git a/.vscode/launch.json b/.vscode/launch.json index fe98036b66..8e2238222f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,7 @@ ], "stopOnEntry": false, "sourceMaps": true, - "outDir": "out" + "outDir": "${workspaceRoot}/out" } ] } \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000000..76287b7c1c --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,12 @@ +**/*.gitignore +tsconfig.json + +src/** +**/*.map + +.vscode/** + +coreclr-debug/debugAdapters/** +coreclr-debug/bin/** +coreclr-debug/obj/** +coreclr-debug/project.lock.json \ No newline at end of file diff --git a/coreclr-debug/.gitignore b/coreclr-debug/.gitignore new file mode 100644 index 0000000000..4486b48fcd --- /dev/null +++ b/coreclr-debug/.gitignore @@ -0,0 +1,4 @@ +bin +obj +project.lock.json +debugAdapters \ No newline at end of file diff --git a/coreclr-debug/NuGet.config b/coreclr-debug/NuGet.config new file mode 100644 index 0000000000..ae37d1c524 --- /dev/null +++ b/coreclr-debug/NuGet.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/coreclr-debug/dummy.cs b/coreclr-debug/dummy.cs new file mode 100644 index 0000000000..15513fc2c5 --- /dev/null +++ b/coreclr-debug/dummy.cs @@ -0,0 +1,13 @@ +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/coreclr-debug/project.json b/coreclr-debug/project.json new file mode 100644 index 0000000000..a4d8a8eb1f --- /dev/null +++ b/coreclr-debug/project.json @@ -0,0 +1,33 @@ +{ + "name": "dummy", + "compilationOptions": { + "emitEntryPoint": true + }, + "dependencies": { + "Microsoft.VisualStudio.clrdbg": "14.0.25017-pb-2822693", + "Microsoft.VisualStudio.clrdbg.MIEngine": "14.0.30217-pb-1", + "Microsoft.VisualStudio.OpenDebugAD7": "1.0.20217-pb-1", + "NETStandard.Library": "1.0.0-rc3-23803", + "Newtonsoft.Json": "7.0.1", + "Microsoft.VisualStudio.Debugger.Interop.Portable": "1.0.1", + "System.Collections.Specialized": "4.0.1-rc3-23803", + "System.Collections.Immutable": "1.2.0-rc3-23803", + "System.Diagnostics.Process" : "4.1.0-rc3-23803", + "System.Diagnostics.StackTrace": "4.0.1-rc3-23803", + "System.Dynamic.Runtime": "4.0.11-rc3-23803", + "Microsoft.CSharp": "4.0.1-rc3-23803", + "System.Threading.Tasks.Dataflow": "4.6.0-rc3-23803", + "System.Threading.Thread": "4.0.0-rc3-23803", + "System.Xml.XDocument": "4.0.11-rc3-23803", + "System.Xml.XmlDocument": "4.0.1-rc3-23803", + "System.Xml.XmlSerializer": "4.0.11-rc3-23803", + "System.ComponentModel": "4.0.1-rc3-23803", + "System.ComponentModel.Annotations": "4.1.0-rc3-23803", + "System.ComponentModel.EventBasedAsync": "4.0.11-rc3-23803", + "System.Runtime.Serialization.Primitives": "4.1.0-rc3-23803", + "System.Net.Http": "4.0.1-rc3-23803" + }, + "frameworks": { + "dnxcore50": { } + } +} diff --git a/package.json b/package.json index ffb0e4fc40..be8ef75e37 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ }, "activationEvents": [ "onLanguage:csharp", + "onLanguage:fsharp", "onCommand:o.restart", "onCommand:o.pickProjectAndStart", "onCommand:o.restore", @@ -128,6 +129,228 @@ "language": "csharp", "path": "./snippets/csharp.json" } + ], + "debuggers": [ + { + "type": "coreclr", + "label": ".NET Core", + "enableBreakpointsFor": { "languageIds": [ "csharp", "fsharp" ] }, + + "program": "./coreclr-debug/debugAdapters/OpenDebugAD7", + "windows": { + "program": "./coreclr-debug/debugAdapters/OpenDebugAD7.exe" + }, + + "configurationAttributes": { + "launch": { + "required": [ "program", "cwd" ], + "properties": { + "program": { + "type": "string", + "description": "Path to the program (executable file) to launch. On Windows, a '.exe' suffix is appended if not specified already.", + "default": "${workspaceRoot}/bin/Debug/dnxcore50/" + }, + "cwd": { + "type": "string", + "description": "Path to the working directory of the program being debugged. Default is the current workspace.", + "default": "${workspaceRoot}" + }, + "args": { + "type": "array", + "description": "Command line arguments passed to the program.", + "items": { "type": "string" }, + "default": [ ] + }, + "stopAtEntry": { + "type": "boolean", + "description": "If true, the debugger should stop at the entry point of the target.", + "default": false + }, + "launchBrowser": { + "type": "object", + "description": "Describes options to launch a web browser as part of launch", + "default": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether web browser launch is enabled", + "default": true + }, + "args": { + "type": "string", + "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "default": "${auto-detect-url}" + }, + "osx": { + "type": "object", + "description": "OSX-specific web launch configuration options", + "default": { + "command": "open" + }, + "properties": { + "command": { + "type": "string", + "description": "The command to execute for launching the web browser", + "default": "open" + }, + "args": { + "type": "string", + "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "default": "${auto-detect-url}" + } + } + }, + "linux": { + "type": "object", + "description": "Linux-specific web launch configuration options", + "default": { + "command": "xdg-open" + }, + "properties": { + "command": { + "type": "string", + "description": "The command to execute for launching the web browser", + "default": "xdg-open" + }, + "args": { + "type": "string", + "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "default": "${auto-detect-url}" + } + } + }, + "windows": { + "type": "object", + "description": "Windows-specific web launch configuration options", + "default": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "properties": { + "command": { + "type": "string", + "description": "The command to execute for launching the web browser", + "default": "cmd.exe" + }, + "args": { + "type": "string", + "description": "The arguments to pass to the command to open the browser. Use ${auto-detect-url} to automatically use the address the server is listening to", + "default": "/C start ${auto-detect-url}" + } + } + } + } + }, + "sourceFileMap": { + "type": "object", + "description": "Optional source file mappings passed to the debug engine.", + "default": { } + }, + "justMyCode": { + "type": "boolean", + "description": "Optional flag to only show user code.", + "default": true + }, + "symbolPath": { + "type": "array", + "description": "Array of directories to use to search for .pdb files. These directories will be searched in addition to the default locations -- next to the module and the path where the pdb was originally dropped to. Example: '[ \"/Volumes/symbols\" ]", + "items": { "type": "string" }, + "default": [ ] + } + } + }, + "attach": { + "required": [ ], + "properties": { + "processName": { + "type": "string", + "description": "", + "default": "The process name to attach to. If this is used, 'processId' should not be used." + }, + "processId": { + "type": "integer", + "description": "The process id to attach to. If this is used, 'processName' should not be used.", + "default": "" + }, + "sourceFileMap": { + "type": "object", + "description": "Optional source file mappings passed to the debug engine.", + "default": { } + }, + "justMyCode": { + "type": "boolean", + "description": "Optional flag to only show user code.", + "default": true + }, + "symbolPath": { + "type": "array", + "description": "Array of directories to use to search for .pdb files. These directories will be searched in addition to the default locations -- next to the module and the path where the pdb was originally dropped to. Example: '[ \"~/symbols\" ]", + "items": { "type": "string" }, + "default": [ ] + } + } + } + }, + + "initialConfigurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/dnxcore50/", + "args": [ ], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "sourceFileMap": { } + }, + { + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/dnxcore50/", + "args": [ ], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "sourceFileMap": { }, + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processName": "", + "sourceFileMap": { } + } + ] + } ] } } \ No newline at end of file diff --git a/src/coreclr-debug.ts b/src/coreclr-debug.ts new file mode 100644 index 0000000000..ded6f8096b --- /dev/null +++ b/src/coreclr-debug.ts @@ -0,0 +1,243 @@ +'use strict'; + +import * as vscode from 'vscode'; +import * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +var _coreClrDebugDir: string; +var _debugAdapterDir: string; +var _channel: vscode.OutputChannel; + +export function installCoreClrDebug(context: vscode.ExtensionContext) { + _coreClrDebugDir = path.join(context.extensionPath, 'coreclr-debug'); + _debugAdapterDir = path.join(_coreClrDebugDir, 'debugAdapters'); + _channel = vscode.window.createOutputChannel('coreclr-debug'); + + if (existsSync(_debugAdapterDir)) { + console.log('.NET Core Debugger tools already installed'); + return; + } + + _channel.appendLine("Downloading and configuring the .NET Core Debugger..."); + _channel.show(vscode.ViewColumn.Three); + + spawnChildProcess('dotnet', ['restore'], _channel, _coreClrDebugDir) + .then(function() { + return spawnChildProcess('dotnet', ['publish', '-o', _debugAdapterDir], _channel, _coreClrDebugDir); + }).then(function() { + var promises: Promise[] = []; + + promises.push(renameDummyEntrypoint()); + promises = promises.concat(copyFiles(context)); + promises.push(removeLibCoreClrTraceProvider()); + + return Promise.all(promises); + }).then(function() { + _channel.appendLine('Succesfully installed .NET Core Debugger.'); + }) + .catch(function(error) { + _channel.appendLine('Error while installing .NET Core Debugger.'); + console.log(error); + }); +} + +function renameDummyEntrypoint() : Promise { + var src = path.join(_debugAdapterDir, 'dummy'); + var dest = path.join(_debugAdapterDir, 'OpenDebugAD7'); + + src += getPlatformExeExtension(); + dest += getPlatformExeExtension(); + + var promise = new Promise(function(resolve, reject) { + fs.rename(src, dest, function(err) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + return promise; +} + +function copyFiles(context: vscode.ExtensionContext): Promise[] { + // TODO: This method and all invocations can be removed once + // the dotnet cli tools support nuget packages with + // contentFiles metadata + // https://docs.nuget.org/create/nuspec-reference#contentfiles-with-visual-studio-2015-update-1-and-later + + var projectJson = JSON.parse(fs.readFileSync(path.join(_coreClrDebugDir, 'project.json')).toString()); + + var clrdbgId = 'Microsoft.VisualStudio.clrdbg'; + var clrdbgVersion: string = projectJson.dependencies[clrdbgId]; + + var MIEngineId = 'Microsoft.VisualStudio.clrdbg.MIEngine'; + var MIEngineVersion: string = projectJson.dependencies[MIEngineId]; + + var destRoot: string = _debugAdapterDir; + + var packagesRoot = getPackagesRoot(); + var clrdbgRoot = path.join(packagesRoot, clrdbgId, clrdbgVersion); + var MIEngineRoot = path.join(packagesRoot, MIEngineId, MIEngineVersion); + + var promises: Promise[] = []; + + function copyClrdbg(src: string, dest?: string) { promises.push(copy(path.join(clrdbgRoot, src), dest)); } + function copyMIEngine(src: string, dest?: string) { promises.push(copy(path.join(MIEngineRoot, src), dest)); } + + copyClrdbg(path.join('1033', 'clrdbg.resources.dll'), '1033'); + copyClrdbg(path.join('1033', 'vsdebugeng.impl.resources.dll'), '1033'); + copyClrdbg(path.join('1033', 'VSDebugUI.dll'), '1033'); + copyClrdbg('libclrdbg.vsdconfig'); + copyClrdbg('Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.ExpressionCompiler.vsdconfig'); + copyClrdbg('Microsoft.CodeAnalysis.CSharp.ExpressionEvaluator.ResultProvider.vsdconfig'); + copyClrdbg('version.txt'); + copyClrdbg('vsdebugeng.impl.vsdconfig'); + copyClrdbg('vsdebugeng.manimpl.vsdconfig'); + + copyMIEngine('coreclr.ad7Engine.json'); + + return promises; +} + +function removeLibCoreClrTraceProvider() : Promise +{ + var filePath = path.join(_debugAdapterDir, 'libcoreclrtraceptprovider' + getPlatformLibExtension()); + + if (!existsSync(filePath)) { + return Promise.resolve(); + } else { + return new Promise(function(resolve, reject) { + fs.unlink(filePath, function(err) { + if (err) { + reject(err); + } else { + _channel.appendLine('Succesfully deleted ' + filePath) + resolve(); + } + }); + }); + } +} + +function copy(src: string, dest?: string) : Promise { + var destination = _debugAdapterDir; + if (dest) { + destination = path.join(destination, dest); + } + + return new Promise(function(resolve, reject) { + + if (!existsSync(destination)) { + fs.mkdirSync(destination); + } + + var destFile = path.join(destination, path.basename(src)) + + var sourceStream = fs.createReadStream(src); + sourceStream.on('error', reject); + var destStream = fs.createWriteStream(destFile); + destStream.on('error', reject); + destStream.on('finish', function() { + _channel.appendLine('Succesfully copied ' + src + ' to ' + destination); + resolve(); + }); + sourceStream.pipe(destStream); + }); +} + +// TODO: not currently used but may need to be used for updating. +// TODO: wrap this in try catch for i/o errors +function deleteDirectoryRecursivelySync(dirPath: string) { + if (existsSync(dirPath)) { + fs.readdirSync(dirPath).forEach(function(file, index) { + var currentPath = path.join(dirPath, file); + if (fs.lstatSync(currentPath).isDirectory()) { + deleteDirectoryRecursivelySync(currentPath); + } + else { + fs.unlinkSync(currentPath); + } + }); + fs.rmdirSync(dirPath); + } +} + +function existsSync(path: string) : boolean { + try { + fs.accessSync(path, fs.F_OK); + return true; + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } else { + throw err; + } + } +} + +function getPackagesRoot() : string { + var homedir = getHomeDir(); + return path.join(homedir, '.nuget', 'packages'); +} + +function getHomeDir() : string { + switch (process.platform) { + case "win32": + return process.env['USERPROFILE']; + case "darwin": + case "linux": + return process.env['HOME']; + default: + throw new Error('Unsupported platform: ' + process.platform); + } +} + +function getPlatformExeExtension() : string { + if (process.platform === 'win32') { + return '.exe' + } + + return ''; +} + +function getPlatformLibExtension() : string { + switch (process.platform) { + case 'win32': + return '.dll'; + case 'darwin': + return '.dylib'; + case 'linux': + return '.so'; + default: + throw Error('Unsupported platform ' + process.platform); + } +} + +function spawnChildProcess(process: string, args: string[], channel: vscode.OutputChannel, workingDirectory: string) : Promise { + var promise = new Promise( function (resolve, reject) { + const child = child_process.spawn(process, args, {cwd: workingDirectory}); + + child.stdout.on('data', (data) => { + channel.append(`${data}`); + }); + + child.stderr.on('data', (data) => { + channel.appendLine(`Error: ${data}`); + }); + + child.on('close', (code: number) => { + if (code != 0) { + channel.appendLine(`${process} exited with error code ${code}`); + reject(new Error(code.toString())); + } + else { + resolve(); + } + }); + }); + + return promise; +} \ No newline at end of file diff --git a/src/omnisharpMain.ts b/src/omnisharpMain.ts index 5c31ee507c..6ce667295d 100644 --- a/src/omnisharpMain.ts +++ b/src/omnisharpMain.ts @@ -24,6 +24,7 @@ import forwardChanges from './features/changeForwarding'; import reportStatus from './features/omnisharpStatus'; import findLaunchTargets from './launchTargetFinder'; import {Disposable, ExtensionContext, DocumentSelector, languages, extensions} from 'vscode'; +import {installCoreClrDebug} from './coreclr-debug'; export function activate(context: ExtensionContext): any { @@ -75,6 +76,9 @@ export function activate(context: ExtensionContext): any { advisor.dispose(); server.stop(); })); + + // install coreclr-debug + installCoreClrDebug(context); context.subscriptions.push(...disposables); } diff --git a/tsconfig.json b/tsconfig.json index 73d8ec0fc6..a6e763bd2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "noLib": true, - "target": "ES5", + "target": "es5", "module": "commonjs", "outDir": "out", "sourceMap": true