55
66import { OmniSharpServer } from '../omnisharp/server' ;
77import { toRange } from '../omnisharp/typeConvertion' ;
8+ import { DebuggerEventsProtocol } from '../coreclr-debug/debuggerEventsProtocol' ;
89import * as vscode from 'vscode' ;
910import * as serverUtils from "../omnisharp/utils" ;
1011import * as protocol from '../omnisharp/protocol' ;
1112import * as utils from '../common' ;
13+ import * as net from 'net' ;
14+ import * as os from 'os' ;
15+ import * as path from 'path' ;
1216
1317let _testOutputChannel : vscode . OutputChannel = undefined ;
1418
@@ -66,29 +70,33 @@ export function runDotnetTest(testMethod: string, fileName: string, testFramewor
6670 } ) ;
6771}
6872
69- function createLaunchConfiguration ( program : string , argsString : string , cwd : string ) {
73+ function createLaunchConfiguration ( program : string , argsString : string , cwd : string , debuggerEventsPipeName : string ) {
7074 let args = utils . splitCommandLineArgs ( argsString ) ;
7175
7276 return {
77+ // NOTE: uncomment this for vsdbg developement
78+ // debugServer: 4711,
7379 name : ".NET Test Launch" ,
7480 type : "coreclr" ,
7581 request : "launch" ,
82+ debuggerEventsPipeName : debuggerEventsPipeName ,
7683 program,
7784 args,
7885 cwd,
7986 stopAtEntry : true
8087 } ;
8188}
8289
83- function getLaunchConfigurationForVSTest ( server : OmniSharpServer , fileName : string , testMethod : string , testFrameworkName : string ) : Promise < any > {
90+ function getLaunchConfigurationForVSTest ( server : OmniSharpServer , fileName : string , testMethod : string , testFrameworkName : string , debugEventListener : DebugEventListener ) : Promise < any > {
91+
8492 const request : protocol . V2 . DebugTestGetStartInfoRequest = {
8593 FileName : fileName ,
8694 MethodName : testMethod ,
8795 TestFrameworkName : testFrameworkName
8896 } ;
8997
9098 return serverUtils . debugTestGetStartInfo ( server , request )
91- . then ( response => createLaunchConfiguration ( response . FileName , response . Arguments , response . WorkingDirectory ) ) ;
99+ . then ( response => createLaunchConfiguration ( response . FileName , response . Arguments , response . WorkingDirectory , debugEventListener . pipePath ( ) ) ) ;
92100}
93101
94102function getLaunchConfigurationForLegacy ( server : OmniSharpServer , fileName : string , testMethod : string , testFrameworkName : string ) : Promise < any > {
@@ -99,16 +107,16 @@ function getLaunchConfigurationForLegacy(server: OmniSharpServer, fileName: stri
99107 } ;
100108
101109 return serverUtils . getTestStartInfo ( server , request )
102- . then ( response => createLaunchConfiguration ( response . Executable , response . Argument , response . WorkingDirectory ) ) ;
110+ . then ( response => createLaunchConfiguration ( response . Executable , response . Argument , response . WorkingDirectory , null ) ) ;
103111}
104112
105113
106- function getLaunchConfiguration ( server : OmniSharpServer , debugType : string , fileName : string , testMethod : string , testFrameworkName : string ) : Promise < any > {
114+ function getLaunchConfiguration ( server : OmniSharpServer , debugType : string , fileName : string , testMethod : string , testFrameworkName : string , debugEventListener : DebugEventListener ) : Promise < any > {
107115 switch ( debugType ) {
108116 case "legacy" :
109117 return getLaunchConfigurationForLegacy ( server , fileName , testMethod , testFrameworkName ) ;
110118 case "vstest" :
111- return getLaunchConfigurationForVSTest ( server , fileName , testMethod , testFrameworkName ) ;
119+ return getLaunchConfigurationForVSTest ( server , fileName , testMethod , testFrameworkName , debugEventListener ) ;
112120
113121 default :
114122 throw new Error ( `Unexpected debug type: ${ debugType } ` ) ;
@@ -120,30 +128,35 @@ export function debugDotnetTest(testMethod: string, fileName: string, testFramew
120128 // We support to styles of 'dotnet test' for debugging: The legacy 'project.json' testing, and the newer csproj support
121129 // using VS Test. These require a different level of communication.
122130 let debugType : string ;
131+ let debugEventListener : DebugEventListener = null ;
132+ let outputChannel = getTestOutputChannel ( ) ;
133+ outputChannel . appendLine ( `Debugging method '${ testMethod } '.` ) ;
123134
124135 return serverUtils . requestProjectInformation ( server , { FileName : fileName } )
125136 . then ( projectInfo => {
126137 if ( projectInfo . DotNetProject ) {
127138 debugType = "legacy" ;
139+ return Promise . resolve ( ) ;
128140 }
129141 else if ( projectInfo . MsBuildProject ) {
130142 debugType = "vstest" ;
143+ debugEventListener = new DebugEventListener ( fileName , server , outputChannel ) ;
144+ return debugEventListener . start ( ) ;
131145 }
132146 else {
133147 throw new Error ( ) ;
134148 }
135-
136- return getLaunchConfiguration ( server , debugType , fileName , testMethod , testFrameworkName ) ;
137149 } )
138- . then ( config => vscode . commands . executeCommand ( 'vscode.startDebug' , config ) )
139150 . then ( ( ) => {
140- // For VS Test, we need to signal to start the test run after the debugger has launched.
141- // TODO: Need to find out when the debugger has actually launched. This is currently a race.
142- if ( debugType === "vstest" ) {
143- serverUtils . debugTestRun ( server , { FileName : fileName } ) ;
144- }
151+ return getLaunchConfiguration ( server , debugType , fileName , testMethod , testFrameworkName , debugEventListener ) ;
145152 } )
146- . catch ( reason => vscode . window . showErrorMessage ( `Failed to start debugger: ${ reason } ` ) ) ;
153+ . then ( config => vscode . commands . executeCommand ( 'vscode.startDebug' , config ) )
154+ . catch ( reason => {
155+ vscode . window . showErrorMessage ( `Failed to start debugger: ${ reason } ` ) ;
156+ if ( debugEventListener != null ) {
157+ debugEventListener . close ( ) ;
158+ }
159+ } ) ;
147160}
148161
149162export function updateCodeLensForTest ( bucket : vscode . CodeLens [ ] , fileName : string , node : protocol . Node , isDebugEnable : boolean ) {
@@ -166,4 +179,118 @@ export function updateCodeLensForTest(bucket: vscode.CodeLens[], fileName: strin
166179 { title : "debug test" , command : 'dotnet.test.debug' , arguments : [ testFeature . Data , fileName , testFrameworkName ] } ) ) ;
167180 }
168181 }
182+ }
183+
184+ class DebugEventListener {
185+ static s_activeInstance : DebugEventListener = null ;
186+ _fileName : string ;
187+ _server : OmniSharpServer ;
188+ _outputChannel : vscode . OutputChannel ;
189+ _pipePath : string ;
190+
191+ _serverSocket : net . Server ;
192+ _isClosed : boolean = false ;
193+
194+ constructor ( fileName : string , server : OmniSharpServer , outputChannel : vscode . OutputChannel ) {
195+ this . _fileName = fileName ;
196+ this . _server = server ;
197+ this . _outputChannel = outputChannel ;
198+ // NOTE: The max pipe name on OSX is fairly small, so this name shouldn't bee too long.
199+ const pipeSuffix = "TestDebugEvents-" + process . pid ;
200+ if ( os . platform ( ) === 'win32' ) {
201+ this . _pipePath = "\\\\.\\pipe\\Microsoft.VSCode.CSharpExt." + pipeSuffix ;
202+ } else {
203+ this . _pipePath = path . join ( utils . getExtensionPath ( ) , "." + pipeSuffix ) ;
204+ }
205+ }
206+
207+ public start ( ) : Promise < void > {
208+
209+ // We use our process id as part of the pipe name, so if we still somehow have an old instance running, close it.
210+ if ( DebugEventListener . s_activeInstance !== null ) {
211+ DebugEventListener . s_activeInstance . close ( ) ;
212+ }
213+ DebugEventListener . s_activeInstance = this ;
214+
215+ this . _serverSocket = net . createServer ( ( socket : net . Socket ) => {
216+ socket . on ( 'data' , ( buffer : Buffer ) => {
217+
218+ let event : DebuggerEventsProtocol . DebuggerEvent ;
219+ try {
220+ event = DebuggerEventsProtocol . decodePacket ( buffer ) ;
221+ } catch ( e ) {
222+ this . _outputChannel . appendLine ( "Warning: Invalid event received from debugger" ) ;
223+ return ;
224+ }
225+
226+ if ( event . eventType === DebuggerEventsProtocol . EventType . ProcessLaunched ) {
227+ let processLaunchedEvent = < DebuggerEventsProtocol . ProcessLaunchedEvent > ( event ) ;
228+ this . _outputChannel . appendLine ( `Started debugging process #${ processLaunchedEvent . targetProcessId } .` ) ;
229+ // TODO: provide the process id to OmniSharp
230+ serverUtils . debugTestRun ( this . _server , { FileName : this . _fileName } ) ;
231+ } else if ( event . eventType === DebuggerEventsProtocol . EventType . DebuggingStopped ) {
232+ this . _outputChannel . appendLine ( "Debugging complete." ) ;
233+ this . fireDebuggingStopped ( ) ;
234+ }
235+ } ) ;
236+
237+ socket . on ( 'end' , ( ) => {
238+ this . fireDebuggingStopped ( ) ;
239+ } ) ;
240+ } ) ;
241+
242+ return this . removeSocketFileIfExists ( ) . then ( ( ) => {
243+ return new Promise < void > ( ( resolve , reject ) => {
244+ let isStarted : boolean = false ;
245+ this . _serverSocket . on ( 'error' , ( err : Error ) => {
246+ if ( ! isStarted ) {
247+ reject ( err . message ) ;
248+ } else {
249+ this . _outputChannel . appendLine ( "Warning: Communications error on debugger event channel. " + err . message ) ;
250+ }
251+ } ) ;
252+ this . _serverSocket . listen ( this . _pipePath , ( ) => {
253+ isStarted = true ;
254+ resolve ( ) ;
255+ } ) ;
256+ } ) ;
257+ } ) ;
258+ }
259+
260+ public pipePath ( ) : string {
261+ return this . _pipePath ;
262+ }
263+
264+ public close ( ) {
265+ if ( this === DebugEventListener . s_activeInstance ) {
266+ DebugEventListener . s_activeInstance = null ;
267+ }
268+ if ( this . _isClosed ) {
269+ return ;
270+ }
271+ this . _isClosed = true ;
272+
273+ if ( this . _serverSocket !== null ) {
274+ this . _serverSocket . close ( ) ;
275+ }
276+ }
277+
278+ private fireDebuggingStopped ( ) : void {
279+ if ( this . _isClosed ) {
280+ return ;
281+ }
282+
283+ // TODO: notify omniSharp
284+
285+ this . close ( ) ;
286+ }
287+
288+ private removeSocketFileIfExists ( ) : Promise < void > {
289+ if ( os . platform ( ) === 'win32' ) {
290+ // Win32 doesn't use the file system for pipe names
291+ return Promise . resolve ( ) ;
292+ } else {
293+ return utils . deleteIfExists ( this . _pipePath ) ;
294+ }
295+ }
169296}
0 commit comments