Skip to content

Commit 61527d6

Browse files
committed
WIP
1 parent 21ba944 commit 61527d6

File tree

5 files changed

+1178
-13
lines changed

5 files changed

+1178
-13
lines changed

debug/dbgp.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { EventEmitter } from "events";
2+
import * as iconv from "iconv-lite";
3+
import * as net from "net";
4+
import { DOMParser } from "xmldom";
5+
6+
/** The encoding all XDebug messages are encoded with */
7+
export const ENCODING = "iso-8859-1";
8+
9+
/** The two states the connection switches between */
10+
enum ParsingState {
11+
DataLength,
12+
Response,
13+
}
14+
15+
/** Wraps the NodeJS Socket and calls handleResponse() whenever a full response arrives */
16+
export class DbgpConnection extends EventEmitter {
17+
private _socket: net.Socket;
18+
private _parsingState: ParsingState;
19+
private _chunksDataLength: number;
20+
private _chunks: Buffer[];
21+
private _dataLength: number;
22+
23+
constructor(socket: net.Socket) {
24+
super();
25+
this._socket = socket;
26+
this._parsingState = ParsingState.DataLength;
27+
this._chunksDataLength = 0;
28+
this._chunks = [];
29+
socket.on("data", (data: Buffer) => this._handleDataChunk(data));
30+
socket.on("error", (error: Error) => this.emit("error", error));
31+
socket.on("close", () => this.emit("close"));
32+
}
33+
34+
public write(command: Buffer): Promise<void> {
35+
return new Promise<void>((resolve, reject) => {
36+
if (this._socket.writable) {
37+
this._socket.write(command, () => {
38+
resolve();
39+
});
40+
} else {
41+
reject(new Error("socket not writable"));
42+
}
43+
});
44+
}
45+
46+
/** closes the underlying socket */
47+
public close(): Promise<void> {
48+
return new Promise<void>((resolve, reject) => {
49+
this._socket.once("close", resolve);
50+
this._socket.end();
51+
});
52+
}
53+
54+
private _handleDataChunk(data: Buffer) {
55+
// Anatomy of packets: [data length] [NULL] [xml] [NULL]
56+
// are we waiting for the data length or for the response?
57+
if (this._parsingState === ParsingState.DataLength) {
58+
// does data contain a NULL byte?
59+
const nullByteIndex = data.indexOf(0);
60+
if (nullByteIndex !== -1) {
61+
// YES -> we received the data length and are ready to receive the response
62+
const lastPiece = data.slice(0, nullByteIndex);
63+
this._chunks.push(lastPiece);
64+
this._chunksDataLength += lastPiece.length;
65+
this._dataLength = parseInt(iconv.decode(Buffer.concat(this._chunks, this._chunksDataLength), ENCODING), 10);
66+
// reset buffered chunks
67+
this._chunks = [];
68+
this._chunksDataLength = 0;
69+
// switch to response parsing state
70+
this._parsingState = ParsingState.Response;
71+
// if data contains more info (except the NULL byte)
72+
if (data.length > nullByteIndex + 1) {
73+
// handle the rest of the packet as part of the response
74+
const rest = data.slice(nullByteIndex + 1);
75+
this._handleDataChunk(rest);
76+
}
77+
} else {
78+
// NO -> this is only part of the data length. We wait for the next data event
79+
this._chunks.push(data);
80+
this._chunksDataLength += data.length;
81+
}
82+
} else if (this._parsingState === ParsingState.Response) {
83+
// does the new data together with the buffered data add up to the data length?
84+
if (this._chunksDataLength + data.length >= this._dataLength) {
85+
// YES -> we received the whole response
86+
// append the last piece of the response
87+
const lastResponsePiece = data.slice(0, this._dataLength - this._chunksDataLength);
88+
this._chunks.push(lastResponsePiece);
89+
this._chunksDataLength += data.length;
90+
const response = Buffer.concat(this._chunks, this._chunksDataLength);
91+
// call response handler
92+
const xml = iconv.decode(response, ENCODING);
93+
const parser = new DOMParser({
94+
errorHandler: {
95+
error: (error) => {
96+
this.emit("error", error instanceof Error ? error : new Error(error));
97+
},
98+
fatalError: (error) => {
99+
this.emit("error", error instanceof Error ? error : new Error(error));
100+
},
101+
warning: (warning) => {
102+
this.emit("warning", warning);
103+
},
104+
},
105+
});
106+
const document = parser.parseFromString(xml, "application/xml");
107+
this.emit("message", document);
108+
// reset buffer
109+
this._chunks = [];
110+
this._chunksDataLength = 0;
111+
// switch to data length parsing state
112+
this._parsingState = ParsingState.DataLength;
113+
// if data contains more info (except the NULL byte)
114+
if (data.length > lastResponsePiece.length + 1) {
115+
// handle the rest of the packet (after the NULL byte) as data length
116+
const rest = data.slice(lastResponsePiece.length + 1);
117+
this._handleDataChunk(rest);
118+
}
119+
} else {
120+
// NO -> this is not the whole response yet. We buffer it and wait for the next data event.
121+
this._chunks.push(data);
122+
this._chunksDataLength += data.length;
123+
}
124+
}
125+
}
126+
}

debug/index.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as net from "net";
2+
import * as vscode from "vscode-debugadapter";
3+
import { DebugProtocol as VSCodeDebugProtocol } from "vscode-debugprotocol";
4+
5+
interface ILaunchRequestArguments extends VSCodeDebugProtocol.LaunchRequestArguments {
6+
}
7+
8+
class ObjectScriptDebugSession extends vscode.DebugSession {
9+
private _args: ILaunchRequestArguments;
10+
private _server: net.Server;
11+
12+
protected async launchRequest(response: VSCodeDebugProtocol.LaunchResponse, args: ILaunchRequestArguments) {
13+
this._args = args;
14+
/** launches the script as CLI */
15+
const launchScript = async () => { };
16+
17+
/** sets up a TCP server to listen for XDebug connections */
18+
const createServer = () =>
19+
new Promise((resolve, reject) => {
20+
const server = (this._server = net.createServer());
21+
server.on("connection", async (socket: net.Socket) => {
22+
try {
23+
// new XDebug connection
24+
const connection = new xdebug.Connection(socket);
25+
this._connections.set(connection.id, connection);
26+
this._waitingConnections.add(connection);
27+
const disposeConnection = (error?: Error) => {
28+
if (this._connections.has(connection.id)) {
29+
if (error) {
30+
this.sendEvent(
31+
new vscode.OutputEvent(
32+
"connection " + connection.id + ": " + error.message + "\n",
33+
),
34+
);
35+
}
36+
this.sendEvent(new vscode.ThreadEvent("exited", connection.id));
37+
connection.close();
38+
this._connections.delete(connection.id);
39+
this._waitingConnections.delete(connection);
40+
}
41+
};
42+
connection.on("warning", (warning: string) => {
43+
this.sendEvent(new vscode.OutputEvent(warning + "\n"));
44+
});
45+
connection.on("error", disposeConnection);
46+
connection.on("close", disposeConnection);
47+
await connection.waitForInitPacket();
48+
49+
// override features from launch.json
50+
try {
51+
const xdebugSettings = {};
52+
await Promise.all(
53+
Object.keys(xdebugSettings).map((setting) =>
54+
connection.sendFeatureSetCommand(setting, xdebugSettings[setting]),
55+
),
56+
);
57+
} catch (error) {
58+
throw new Error(
59+
"Error applying xdebugSettings: " + (error instanceof Error ? error.message : error),
60+
);
61+
}
62+
63+
this.sendEvent(new vscode.ThreadEvent("started", connection.id));
64+
65+
// request breakpoints from VS Code
66+
await this.sendEvent(new vscode.InitializedEvent());
67+
} catch (error) {
68+
this.sendEvent(
69+
new vscode.OutputEvent((error instanceof Error ? error.message : error) + "\n", "stderr"),
70+
);
71+
this.shutdown();
72+
}
73+
});
74+
server.on("error", (error: Error) => {
75+
// this.sendEvent(new vscode.OutputEvent(util.inspect(error) + "\n"));
76+
// this.sendErrorResponse(response, error as Error);
77+
});
78+
// server.listen(
79+
// 52773,
80+
// "localhost",
81+
// (error: NodeJS.ErrnoException) => (error ? reject(error) : resolve()),
82+
// );
83+
});
84+
try {
85+
if (!args.noDebug) {
86+
await createServer();
87+
}
88+
} catch (error) {
89+
// this.sendErrorResponse(response, error as Error);
90+
return;
91+
}
92+
this.sendResponse(response);
93+
}
94+
}
95+
96+
vscode.DebugSession.run(ObjectScriptDebugSession);

0 commit comments

Comments
 (0)