|
2 | 2 | * Copyright (C) Microsoft Corporation. All rights reserved.
|
3 | 3 | *--------------------------------------------------------*/
|
4 | 4 |
|
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"; |
9 | 7 | 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(); |
56 | 39 | });
|
57 | 40 |
|
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()); |
100 | 48 | }
|
101 |
| - }, |
102 |
| - ); |
103 |
| -} |
104 | 49 |
|
105 |
| -function waitForSessionFile(triesRemaining: number) { |
| 50 | + this._isConnected = true; |
| 51 | + this._logger.writeVerbose("Connected to socket!"); |
| 52 | + }); |
106 | 53 |
|
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 | + } |
108 | 57 |
|
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; |
118 | 62 | }
|
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); |
133 | 65 | }
|
134 |
| -} |
135 | 66 |
|
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 | +} |
0 commit comments