Skip to content

Use in-memory debug adapter instead of spinning up new process #2672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"publisher": "ms-vscode",
"description": "(Preview) Develop PowerShell scripts in Visual Studio Code!",
"engines": {
"vscode": "^1.40.0"
"vscode": "^1.43.0"
},
"license": "SEE LICENSE IN LICENSE.txt",
"homepage": "https://github.com/PowerShell/vscode-powershell/blob/master/README.md",
Expand All @@ -28,7 +28,7 @@
"main": "./out/src/main",
"activationEvents": [
"onDebugInitialConfigurations",
"onDebugResolve:powershell",
"onDebugResolve:PowerShell",
"onLanguage:powershell",
"onCommand:PowerShell.NewProjectFromTemplate",
"onCommand:PowerShell.OpenExamplesFolder",
Expand Down Expand Up @@ -57,7 +57,7 @@
"@types/rewire": "~2.5.28",
"@types/semver": "~7.1.0",
"@types/sinon": "~9.0.0",
"@types/vscode": "1.40.0",
"@types/vscode": "1.43.0",
"mocha": "~5.2.0",
"mocha-junit-reporter": "~1.23.3",
"mocha-multi-reporters": "~1.1.7",
Expand Down Expand Up @@ -377,8 +377,6 @@
{
"type": "PowerShell",
"label": "PowerShell",
"program": "./out/src/debugAdapter.js",
"runtime": "node",
"variables": {
"PickPSHostProcess": "PowerShell.PickPSHostProcess",
"PickRunspace": "PowerShell.PickRunspace",
Expand Down
227 changes: 104 additions & 123 deletions src/debugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,136 +2,117 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import fs = require("fs");
import net = require("net");
import os = require("os");
import path = require("path");
import { connect, Socket } from "net";
import { DebugAdapter, Event, DebugProtocolMessage, EventEmitter } from "vscode";
import { Logger } from "./logging";
import utils = require("./utils");

// NOTE: The purpose of this file is to serve as a bridge between
// VS Code's debug adapter client (which communicates via stdio) and
// PowerShell Editor Services' debug service (which communicates via
// named pipes or a network protocol). It is purely a naive data
// relay between the two transports.

const logBasePath = path.resolve(__dirname, "../../logs");

const debugAdapterLogWriter =
fs.createWriteStream(
path.resolve(
logBasePath,
"DebugAdapter.log"));

// Pause the stdin buffer until we're connected to the
// debug server
process.stdin.pause();

const debugSessionFilePath = utils.getDebugSessionFilePath();
debugAdapterLogWriter.write("Session file path: " + debugSessionFilePath + ", pid: " + process.pid + " \r\n");

function startDebugging() {
// Read the details of the current session to learn
// the connection details for the debug service
const sessionDetails = utils.readSessionFile(debugSessionFilePath);

// TODO: store session details into an in-memory store that can be shared between
// the debug adapter and client extension
// and then clean up the session details file.

// Establish connection before setting up the session
debugAdapterLogWriter.write("Connecting to pipe: " + sessionDetails.debugServicePipeName + "\r\n");

let isConnected = false;
const debugServiceSocket = net.connect(sessionDetails.debugServicePipeName);

// Write any errors to the log file
debugServiceSocket.on(
"error",
(e) => {
debugAdapterLogWriter.write("Socket ERROR: " + e + "\r\n");
debugAdapterLogWriter.close();
debugServiceSocket.destroy();
process.exit(0);

export class NamedPipeDebugAdapter implements DebugAdapter {
private static readonly TWO_CRLF = '\r\n\r\n';
private static readonly HEADER_LINESEPARATOR = /\r?\n/; // allow for non-RFC 2822 conforming line separators
private static readonly HEADER_FIELDSEPARATOR = /: */;

private readonly _logger: Logger;
private readonly _namedPipe: string;

private _rawData = Buffer.allocUnsafe(0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the initialisation here used for? I'm guessing there's somewhere we can pass in undefined and this is the cheapest way to pass in an empty buffer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, this comes from VS Code's implementation that's quite challenging to fully understand... they also set this in the constructor... so I'm really unsure why.

private _contentLength = -1;
private _isConnected: boolean = false;
private _debugMessageQueue: DebugProtocolMessage[] = [];

private _debugServiceSocket: Socket;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be added to the constructor for injection-initialisation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided against this because I wanted the _ prefix.


// The event that VS Code-proper will listen for.
private _sendMessage: EventEmitter<DebugProtocolMessage> = new EventEmitter<DebugProtocolMessage>();
onDidSendMessage: Event<DebugProtocolMessage> = this._sendMessage.event;

constructor(namedPipe: string, logger: Logger) {
this._namedPipe = namedPipe;
this._logger = logger;
}

public start(): void {
this._debugServiceSocket = connect(this._namedPipe);

this._debugServiceSocket.on("error", (e) => {
this._logger.writeError("Error on Debug Adapter: " + e);
this.dispose();
});

// Route any output from the socket through stdout
debugServiceSocket.on(
"data",
(data: Buffer) => process.stdout.write(data));

// Wait for the connection to complete
debugServiceSocket.on(
"connect",
() => {
isConnected = true;
debugAdapterLogWriter.write("Connected to socket!\r\n\r\n");

// When data comes on stdin, route it through the socket
process.stdin.on(
"data",
(data: Buffer) => debugServiceSocket.write(data));

// Resume the stdin stream
process.stdin.resume();
});

// When the socket closes, end the session
debugServiceSocket.on(
"close",
() => {
debugAdapterLogWriter.write("Socket closed, shutting down.");
debugAdapterLogWriter.close();
isConnected = false;

// Close after a short delay to give the client time
// to finish up
setTimeout(() => {
process.exit(0);
}, 2000);
},
);

process.on(
"exit",
(e) => {
if (debugAdapterLogWriter) {
debugAdapterLogWriter.write("Debug adapter process is exiting...");
// Route any output from the socket through to VS Code.
this._debugServiceSocket.on("data", (data: Buffer) => this.handleData(data));

// Wait for the connection to complete.
this._debugServiceSocket.on("connect", () => {
while(this._debugMessageQueue.length) {
this.writeMessageToDebugAdapter(this._debugMessageQueue.shift());
}
},
);
}

function waitForSessionFile(triesRemaining: number) {
this._isConnected = true;
this._logger.writeVerbose("Connected to socket!");
});

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

if (triesRemaining > 0) {
if (utils.checkIfFileExists(debugSessionFilePath)) {
debugAdapterLogWriter.write(`Session file present, connecting to debug adapter...\r\n\r\n`);
startDebugging();
} else {
// Wait for a second and try again
setTimeout(
() => waitForSessionFile(triesRemaining - 1),
1000);
public handleMessage(message: DebugProtocolMessage): void {
if (!this._isConnected) {
this._debugMessageQueue.push(message);
return;
}
} else {
debugAdapterLogWriter.write(`Timed out waiting for session file!\r\n`);
const errorJson =
JSON.stringify({
type: "response",
request_seq: 1,
command: "initialize",
success: false,
message: "Timed out waiting for the PowerShell extension to start.",
});

process.stdout.write(
`Content-Length: ${Buffer.byteLength(errorJson, "utf8")}\r\n\r\n${errorJson}`,
"utf8");

this.writeMessageToDebugAdapter(message);
}
}

// Wait for the session file to appear
waitForSessionFile(30);
public dispose() {
this._debugServiceSocket.destroy();
this._sendMessage.dispose();
}

private writeMessageToDebugAdapter(message: DebugProtocolMessage): void {
const msg = JSON.stringify(message);
const messageWrapped = `Content-Length: ${Buffer.byteLength(msg, "utf8")}${NamedPipeDebugAdapter.TWO_CRLF}${msg}`;
this._logger.writeDiagnostic(`SENDING TO DEBUG ADAPTER: ${messageWrapped}`);
this._debugServiceSocket.write(messageWrapped, "utf8");
}

// Shamelessly stolen from VS Code's implementation with slight modification by using public types and our logger:
// https://github.com/microsoft/vscode/blob/ff1b513fbca1acad4467dfd768997e9e0b9c5735/src/vs/workbench/contrib/debug/node/debugAdapter.ts#L55-L92
private handleData(data: Buffer): void {
this._rawData = Buffer.concat([this._rawData, data]);

while (true) {
if (this._contentLength >= 0) {
if (this._rawData.length >= this._contentLength) {
const message = this._rawData.toString('utf8', 0, this._contentLength);
this._rawData = this._rawData.slice(this._contentLength);
this._contentLength = -1;
if (message.length > 0) {
try {
this._logger.writeDiagnostic(`RECEIVED FROM DEBUG ADAPTER: ${message}`);
this._sendMessage.fire(JSON.parse(message) as DebugProtocolMessage);
} catch (e) {
this._logger.writeError("Error firing event in VS Code: ", (e.message || e), message);
}
}
continue; // there may be more complete messages to process
}
} else {
const idx = this._rawData.indexOf(NamedPipeDebugAdapter.TWO_CRLF);
if (idx !== -1) {
const header = this._rawData.toString('utf8', 0, idx);
const lines = header.split(NamedPipeDebugAdapter.HEADER_LINESEPARATOR);
for (const h of lines) {
const kvPair = h.split(NamedPipeDebugAdapter.HEADER_FIELDSEPARATOR);
if (kvPair[0] === 'Content-Length') {
this._contentLength = Number(kvPair[1]);
}
}
this._rawData = this._rawData.slice(idx + NamedPipeDebugAdapter.TWO_CRLF.length);
continue;
}
}
break;
}
}
}
31 changes: 24 additions & 7 deletions src/features/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,38 @@ import { PowerShellProcess} from "../process";
import { SessionManager, SessionStatus } from "../session";
import Settings = require("../settings");
import utils = require("../utils");
import { NamedPipeDebugAdapter } from "../debugAdapter";
import { Logger } from "../logging";

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

export class DebugSessionFeature implements IFeature, DebugConfigurationProvider {
export class DebugSessionFeature implements IFeature, DebugConfigurationProvider, vscode.DebugAdapterDescriptorFactory {

private sessionCount: number = 1;
private command: vscode.Disposable;
private tempDebugProcess: PowerShellProcess;
private tempSessionDetails: utils.IEditorServicesSessionDetails;

constructor(context: ExtensionContext, private sessionManager: SessionManager) {
constructor(context: ExtensionContext, private sessionManager: SessionManager, private logger: Logger) {
// Register a debug configuration provider
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("PowerShell", this));
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("PowerShell", this))
}

createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
const sessionDetails = session.configuration.createTemporaryIntegratedConsole
? this.tempSessionDetails
: this.sessionManager.getSessionDetails();

// Establish connection before setting up the session
this.logger.writeVerbose(`Connecting to pipe: ${sessionDetails.debugServicePipeName}`);
this.logger.writeVerbose(`Debug configuration: ${JSON.stringify(session.configuration)}`);

const debugAdapter = new NamedPipeDebugAdapter(sessionDetails.debugServicePipeName, this.logger);
debugAdapter.start();

return new vscode.DebugAdapterInlineImplementation(debugAdapter);
}

public dispose() {
Expand Down Expand Up @@ -295,11 +314,9 @@ export class DebugSessionFeature implements IFeature, DebugConfigurationProvider
sessionFilePath,
settings);

this.tempDebugProcess
.start(`DebugSession-${this.sessionCount++}`)
.then((sessionDetails) => {
utils.writeSessionFile(sessionFilePath, sessionDetails);
});
this.tempSessionDetails = await this.tempDebugProcess.start(`DebugSession-${this.sessionCount++}`);
utils.writeSessionFile(sessionFilePath, this.tempSessionDetails);

} else {
utils.writeSessionFile(sessionFilePath, this.sessionManager.getSessionDetails());
}
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export function activate(context: vscode.ExtensionContext): void {
new CodeActionsFeature(logger),
new NewFileOrProjectFeature(),
new RemoteFilesFeature(),
new DebugSessionFeature(context, sessionManager),
new DebugSessionFeature(context, sessionManager, logger),
new PickPSHostProcessFeature(),
new SpecifyScriptArgsFeature(context),
new HelpCompletionFeature(logger),
Expand Down