Skip to content

Commit a26c09b

Browse files
Merge pull request #1379 from gregg-miskelly/debuggerEventsPipeName
Send events from the debugger to unit test code
2 parents 944cd1a + 974fc89 commit a26c09b

File tree

4 files changed

+206
-34
lines changed

4 files changed

+206
-34
lines changed

src/assets.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -541,27 +541,10 @@ function doesAnyAssetExist(generator: AssetGenerator) {
541541
});
542542
}
543543

544-
function deleteAsset(path: string) {
545-
return new Promise<void>((resolve, reject) => {
546-
fs.exists(path, exists => {
547-
if (exists) {
548-
// TODO: Should we check after unlinking to see if the file still exists?
549-
fs.unlink(path, err => {
550-
if (err) {
551-
return reject(err);
552-
}
553-
554-
resolve();
555-
});
556-
}
557-
});
558-
});
559-
}
560-
561544
function deleteAssets(generator: AssetGenerator) {
562545
return Promise.all([
563-
deleteAsset(generator.launchJsonPath),
564-
deleteAsset(generator.tasksJsonPath)
546+
util.deleteIfExists(generator.launchJsonPath),
547+
util.deleteIfExists(generator.tasksJsonPath)
565548
]);
566549
}
567550

src/common.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,25 @@ export function fileExists(filePath: string): Promise<boolean> {
7373
});
7474
}
7575

76+
export function deleteIfExists(filePath: string): Promise<void> {
77+
return fileExists(filePath)
78+
.then((exists: boolean) => {
79+
return new Promise<void>((resolve, reject) => {
80+
if (!exists) {
81+
resolve();
82+
}
83+
84+
fs.unlink(filePath, err => {
85+
if (err) {
86+
return reject(err);
87+
}
88+
89+
resolve();
90+
});
91+
});
92+
});
93+
}
94+
7695
export enum InstallFileType {
7796
Begin,
7897
Lock
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
'use strict';
7+
8+
// This contains the definition of messages that VsDbg-UI can send back to a listener which registers itself via the 'debuggerEventsPipeName'
9+
// property on a launch or attach request.
10+
//
11+
// All messages are sent as UTF-8 JSON text with a tailing '\n'
12+
export namespace DebuggerEventsProtocol {
13+
export module EventType {
14+
// Indicates that the vsdbg-ui has received the attach or launch request and is starting up
15+
export const Starting = "starting";
16+
// Indicates that vsdbg-ui has successfully launched the specified process.
17+
// The ProcessLaunchedEvent interface details the event payload.
18+
export const ProcessLaunched = "processLaunched";
19+
// Debug session is ending
20+
export const DebuggingStopped = "debuggingStopped";
21+
};
22+
23+
export interface DebuggerEvent {
24+
// Contains one of the 'DebuggerEventsProtocol.EventType' values
25+
eventType: string;
26+
}
27+
28+
export interface ProcessLaunchedEvent extends DebuggerEvent {
29+
// Process id of the newly-launched target process
30+
targetProcessId: number;
31+
}
32+
33+
// Decodes a packet received from the debugger into an event
34+
export function decodePacket(packet: Buffer) : DebuggerEvent {
35+
// Verify the message ends in a newline
36+
if (packet[packet.length-1] != 10 /*\n*/) {
37+
throw new Error("Unexpected message received from debugger.");
38+
}
39+
40+
const message = packet.toString('utf-8', 0, packet.length-1);
41+
return JSON.parse(message);
42+
}
43+
}

src/features/dotnetTest.ts

Lines changed: 142 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
import { OmniSharpServer } from '../omnisharp/server';
77
import { toRange } from '../omnisharp/typeConvertion';
8+
import { DebuggerEventsProtocol } from '../coreclr-debug/debuggerEventsProtocol';
89
import * as vscode from 'vscode';
910
import * as serverUtils from "../omnisharp/utils";
1011
import * as protocol from '../omnisharp/protocol';
1112
import * as utils from '../common';
13+
import * as net from 'net';
14+
import * as os from 'os';
15+
import * as path from 'path';
1216

1317
let _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

94102
function 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

149162
export 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

Comments
 (0)