Skip to content

Commit 8436c5a

Browse files
Use in-memory debug adapter instead of spinning up new process (#2672)
* user in-memory debug adapter instead of spinning up new process * Rob's feedback
1 parent 89b5c64 commit 8436c5a

File tree

5 files changed

+135
-139
lines changed

5 files changed

+135
-139
lines changed

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"publisher": "ms-vscode",
77
"description": "(Preview) Develop PowerShell scripts in Visual Studio Code!",
88
"engines": {
9-
"vscode": "^1.40.0"
9+
"vscode": "^1.43.0"
1010
},
1111
"license": "SEE LICENSE IN LICENSE.txt",
1212
"homepage": "https://github.com/PowerShell/vscode-powershell/blob/master/README.md",
@@ -28,7 +28,7 @@
2828
"main": "./out/src/main",
2929
"activationEvents": [
3030
"onDebugInitialConfigurations",
31-
"onDebugResolve:powershell",
31+
"onDebugResolve:PowerShell",
3232
"onLanguage:powershell",
3333
"onCommand:PowerShell.NewProjectFromTemplate",
3434
"onCommand:PowerShell.OpenExamplesFolder",
@@ -57,7 +57,7 @@
5757
"@types/rewire": "~2.5.28",
5858
"@types/semver": "~7.1.0",
5959
"@types/sinon": "~9.0.0",
60-
"@types/vscode": "1.40.0",
60+
"@types/vscode": "1.43.0",
6161
"mocha": "~5.2.0",
6262
"mocha-junit-reporter": "~1.23.3",
6363
"mocha-multi-reporters": "~1.1.7",
@@ -377,8 +377,6 @@
377377
{
378378
"type": "PowerShell",
379379
"label": "PowerShell",
380-
"program": "./out/src/debugAdapter.js",
381-
"runtime": "node",
382380
"variables": {
383381
"PickPSHostProcess": "PowerShell.PickPSHostProcess",
384382
"PickRunspace": "PowerShell.PickRunspace",

src/debugAdapter.ts

+104-123
Original file line numberDiff line numberDiff line change
@@ -2,136 +2,117 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import fs = require("fs");
6-
import net = require("net");
7-
import os = require("os");
8-
import path = require("path");
5+
import { connect, Socket } from "net";
6+
import { DebugAdapter, Event, DebugProtocolMessage, EventEmitter } from "vscode";
97
import { Logger } from "./logging";
10-
import utils = require("./utils");
11-
12-
// NOTE: The purpose of this file is to serve as a bridge between
13-
// VS Code's debug adapter client (which communicates via stdio) and
14-
// PowerShell Editor Services' debug service (which communicates via
15-
// named pipes or a network protocol). It is purely a naive data
16-
// relay between the two transports.
17-
18-
const logBasePath = path.resolve(__dirname, "../../logs");
19-
20-
const debugAdapterLogWriter =
21-
fs.createWriteStream(
22-
path.resolve(
23-
logBasePath,
24-
"DebugAdapter.log"));
25-
26-
// Pause the stdin buffer until we're connected to the
27-
// debug server
28-
process.stdin.pause();
29-
30-
const debugSessionFilePath = utils.getDebugSessionFilePath();
31-
debugAdapterLogWriter.write("Session file path: " + debugSessionFilePath + ", pid: " + process.pid + " \r\n");
32-
33-
function startDebugging() {
34-
// Read the details of the current session to learn
35-
// the connection details for the debug service
36-
const sessionDetails = utils.readSessionFile(debugSessionFilePath);
37-
38-
// TODO: store session details into an in-memory store that can be shared between
39-
// the debug adapter and client extension
40-
// and then clean up the session details file.
41-
42-
// Establish connection before setting up the session
43-
debugAdapterLogWriter.write("Connecting to pipe: " + sessionDetails.debugServicePipeName + "\r\n");
44-
45-
let isConnected = false;
46-
const debugServiceSocket = net.connect(sessionDetails.debugServicePipeName);
47-
48-
// Write any errors to the log file
49-
debugServiceSocket.on(
50-
"error",
51-
(e) => {
52-
debugAdapterLogWriter.write("Socket ERROR: " + e + "\r\n");
53-
debugAdapterLogWriter.close();
54-
debugServiceSocket.destroy();
55-
process.exit(0);
8+
9+
export class NamedPipeDebugAdapter implements DebugAdapter {
10+
private static readonly TWO_CRLF = '\r\n\r\n';
11+
private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators
12+
private static readonly HEADER_FIELDSEPARATOR = /: */;
13+
14+
private readonly _logger: Logger;
15+
private readonly _namedPipe: string;
16+
17+
private _rawData = Buffer.allocUnsafe(0);
18+
private _contentLength = -1;
19+
private _isConnected: boolean = false;
20+
private _debugMessageQueue: DebugProtocolMessage[] = [];
21+
22+
private _debugServiceSocket: Socket;
23+
24+
// The event that VS Code-proper will listen for.
25+
private _sendMessage: EventEmitter<DebugProtocolMessage> = new EventEmitter<DebugProtocolMessage>();
26+
onDidSendMessage: Event<DebugProtocolMessage> = this._sendMessage.event;
27+
28+
constructor(namedPipe: string, logger: Logger) {
29+
this._namedPipe = namedPipe;
30+
this._logger = logger;
31+
}
32+
33+
public start(): void {
34+
this._debugServiceSocket = connect(this._namedPipe);
35+
36+
this._debugServiceSocket.on("error", (e) => {
37+
this._logger.writeError("Error on Debug Adapter: " + e);
38+
this.dispose();
5639
});
5740

58-
// Route any output from the socket through stdout
59-
debugServiceSocket.on(
60-
"data",
61-
(data: Buffer) => process.stdout.write(data));
62-
63-
// Wait for the connection to complete
64-
debugServiceSocket.on(
65-
"connect",
66-
() => {
67-
isConnected = true;
68-
debugAdapterLogWriter.write("Connected to socket!\r\n\r\n");
69-
70-
// When data comes on stdin, route it through the socket
71-
process.stdin.on(
72-
"data",
73-
(data: Buffer) => debugServiceSocket.write(data));
74-
75-
// Resume the stdin stream
76-
process.stdin.resume();
77-
});
78-
79-
// When the socket closes, end the session
80-
debugServiceSocket.on(
81-
"close",
82-
() => {
83-
debugAdapterLogWriter.write("Socket closed, shutting down.");
84-
debugAdapterLogWriter.close();
85-
isConnected = false;
86-
87-
// Close after a short delay to give the client time
88-
// to finish up
89-
setTimeout(() => {
90-
process.exit(0);
91-
}, 2000);
92-
},
93-
);
94-
95-
process.on(
96-
"exit",
97-
(e) => {
98-
if (debugAdapterLogWriter) {
99-
debugAdapterLogWriter.write("Debug adapter process is exiting...");
41+
// Route any output from the socket through to VS Code.
42+
this._debugServiceSocket.on("data", (data: Buffer) => this.handleData(data));
43+
44+
// Wait for the connection to complete.
45+
this._debugServiceSocket.on("connect", () => {
46+
while(this._debugMessageQueue.length) {
47+
this.writeMessageToDebugAdapter(this._debugMessageQueue.shift());
10048
}
101-
},
102-
);
103-
}
10449

105-
function waitForSessionFile(triesRemaining: number) {
50+
this._isConnected = true;
51+
this._logger.writeVerbose("Connected to socket!");
52+
});
10653

107-
debugAdapterLogWriter.write(`Waiting for session file, tries remaining: ${triesRemaining}...\r\n`);
54+
// When the socket closes, end the session.
55+
this._debugServiceSocket.on("close", () => { this.dispose(); });
56+
}
10857

109-
if (triesRemaining > 0) {
110-
if (utils.checkIfFileExists(debugSessionFilePath)) {
111-
debugAdapterLogWriter.write(`Session file present, connecting to debug adapter...\r\n\r\n`);
112-
startDebugging();
113-
} else {
114-
// Wait for a second and try again
115-
setTimeout(
116-
() => waitForSessionFile(triesRemaining - 1),
117-
1000);
58+
public handleMessage(message: DebugProtocolMessage): void {
59+
if (!this._isConnected) {
60+
this._debugMessageQueue.push(message);
61+
return;
11862
}
119-
} else {
120-
debugAdapterLogWriter.write(`Timed out waiting for session file!\r\n`);
121-
const errorJson =
122-
JSON.stringify({
123-
type: "response",
124-
request_seq: 1,
125-
command: "initialize",
126-
success: false,
127-
message: "Timed out waiting for the PowerShell extension to start.",
128-
});
129-
130-
process.stdout.write(
131-
`Content-Length: ${Buffer.byteLength(errorJson, "utf8")}\r\n\r\n${errorJson}`,
132-
"utf8");
63+
64+
this.writeMessageToDebugAdapter(message);
13365
}
134-
}
13566

136-
// Wait for the session file to appear
137-
waitForSessionFile(30);
67+
public dispose() {
68+
this._debugServiceSocket.destroy();
69+
this._sendMessage.dispose();
70+
}
71+
72+
private writeMessageToDebugAdapter(message: DebugProtocolMessage): void {
73+
const msg = JSON.stringify(message);
74+
const messageWrapped = `Content-Length: ${Buffer.byteLength(msg, "utf8")}${NamedPipeDebugAdapter.TWO_CRLF}${msg}`;
75+
this._logger.writeDiagnostic(`SENDING TO DEBUG ADAPTER: ${messageWrapped}`);
76+
this._debugServiceSocket.write(messageWrapped, "utf8");
77+
}
78+
79+
// Shamelessly stolen from VS Code's implementation with slight modification by using public types and our logger:
80+
// https://github.com/microsoft/vscode/blob/ff1b513fbca1acad4467dfd768997e9e0b9c5735/src/vs/workbench/contrib/debug/node/debugAdapter.ts#L55-L92
81+
private handleData(data: Buffer): void {
82+
this._rawData = Buffer.concat([this._rawData, data]);
83+
84+
while (true) {
85+
if (this._contentLength >= 0) {
86+
if (this._rawData.length >= this._contentLength) {
87+
const message = this._rawData.toString('utf8', 0, this._contentLength);
88+
this._rawData = this._rawData.slice(this._contentLength);
89+
this._contentLength = -1;
90+
if (message.length > 0) {
91+
try {
92+
this._logger.writeDiagnostic(`RECEIVED FROM DEBUG ADAPTER: ${message}`);
93+
this._sendMessage.fire(JSON.parse(message) as DebugProtocolMessage);
94+
} catch (e) {
95+
this._logger.writeError("Error firing event in VS Code: ", (e.message || e), message);
96+
}
97+
}
98+
continue; // there may be more complete messages to process
99+
}
100+
} else {
101+
const idx = this._rawData.indexOf(NamedPipeDebugAdapter.TWO_CRLF);
102+
if (idx !== -1) {
103+
const header = this._rawData.toString('utf8', 0, idx);
104+
const lines = header.split(NamedPipeDebugAdapter.HEADER_LINESEPARATOR);
105+
for (const h of lines) {
106+
const kvPair = h.split(NamedPipeDebugAdapter.HEADER_FIELDSEPARATOR);
107+
if (kvPair[0] === 'Content-Length') {
108+
this._contentLength = Number(kvPair[1]);
109+
}
110+
}
111+
this._rawData = this._rawData.slice(idx + NamedPipeDebugAdapter.TWO_CRLF.length);
112+
continue;
113+
}
114+
}
115+
break;
116+
}
117+
}
118+
}

src/features/DebugSession.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,38 @@ import { PowerShellProcess} from "../process";
1212
import { SessionManager, SessionStatus } from "../session";
1313
import Settings = require("../settings");
1414
import utils = require("../utils");
15+
import { NamedPipeDebugAdapter } from "../debugAdapter";
16+
import { Logger } from "../logging";
1517

1618
export const StartDebuggerNotificationType =
1719
new NotificationType<void, void>("powerShell/startDebugger");
1820

19-
export class DebugSessionFeature implements IFeature, DebugConfigurationProvider {
21+
export class DebugSessionFeature implements IFeature, DebugConfigurationProvider, vscode.DebugAdapterDescriptorFactory {
2022

2123
private sessionCount: number = 1;
2224
private command: vscode.Disposable;
2325
private tempDebugProcess: PowerShellProcess;
26+
private tempSessionDetails: utils.IEditorServicesSessionDetails;
2427

25-
constructor(context: ExtensionContext, private sessionManager: SessionManager) {
28+
constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: Logger) {
2629
// Register a debug configuration provider
2730
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("PowerShell", this));
31+
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("PowerShell", this))
32+
}
33+
34+
createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
35+
const sessionDetails = session.configuration.createTemporaryIntegratedConsole
36+
? this.tempSessionDetails
37+
: this.sessionManager.getSessionDetails();
38+
39+
// Establish connection before setting up the session
40+
this.logger.writeVerbose(`Connecting to pipe: ${sessionDetails.debugServicePipeName}`);
41+
this.logger.writeVerbose(`Debug configuration: ${JSON.stringify(session.configuration)}`);
42+
43+
const debugAdapter = new NamedPipeDebugAdapter(sessionDetails.debugServicePipeName, this.logger);
44+
debugAdapter.start();
45+
46+
return new vscode.DebugAdapterInlineImplementation(debugAdapter);
2847
}
2948

3049
public dispose() {
@@ -295,11 +314,9 @@ export class DebugSessionFeature implements IFeature, DebugConfigurationProvider
295314
sessionFilePath,
296315
settings);
297316

298-
this.tempDebugProcess
299-
.start(`DebugSession-${this.sessionCount++}`)
300-
.then((sessionDetails) => {
301-
utils.writeSessionFile(sessionFilePath, sessionDetails);
302-
});
317+
this.tempSessionDetails = await this.tempDebugProcess.start(`DebugSession-${this.sessionCount++}`);
318+
utils.writeSessionFile(sessionFilePath, this.tempSessionDetails);
319+
303320
} else {
304321
utils.writeSessionFile(sessionFilePath, this.sessionManager.getSessionDetails());
305322
}

src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export function activate(context: vscode.ExtensionContext): void {
156156
new CodeActionsFeature(logger),
157157
new NewFileOrProjectFeature(),
158158
new RemoteFilesFeature(),
159-
new DebugSessionFeature(context, sessionManager),
159+
new DebugSessionFeature(context, sessionManager, logger),
160160
new PickPSHostProcessFeature(),
161161
new SpecifyScriptArgsFeature(context),
162162
new HelpCompletionFeature(logger),

0 commit comments

Comments
 (0)