-
Notifications
You must be signed in to change notification settings - Fork 128
/
Copy pathkuzzleDebugger.ts
300 lines (254 loc) · 8.67 KB
/
kuzzleDebugger.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
import Inspector from "inspector";
import * as kerror from "../../kerror";
import { JSONObject } from "kuzzle-sdk";
import HttpWsProtocol from "../../core/network/protocols/httpwsProtocol";
const DEBUGGER_EVENT = "kuzzle-debugger-event";
export class KuzzleDebugger {
private inspector: Inspector.Session;
private debuggerStatus = false;
/**
* Map<eventName, Set<connectionId>>
*/
private events = new Map<string, Set<string>>();
private httpWsProtocol?: HttpWsProtocol;
async init() {
this.httpWsProtocol = global.kuzzle.entryPoint.protocols.get("websocket");
this.inspector = new Inspector.Session();
// Remove connection id from the list of listeners for each event
global.kuzzle.on("connection:remove", (connectionId) => {
if (!this.debuggerStatus) {
return;
}
for (const listener of this.events.values()) {
listener.delete(connectionId);
}
});
this.inspector.on("inspectorNotification", async (payload) => {
if (!this.debuggerStatus) {
return;
}
await this.notifyGlobalListeners(payload.method, payload);
const listeners = this.events.get(payload.method);
if (!listeners) {
return;
}
const promises = [];
for (const connectionId of listeners) {
promises.push(
this.notifyConnection(connectionId, DEBUGGER_EVENT, {
event: payload.method,
result: payload,
})
);
}
// No need to catch, notify is already try-catched
await Promise.all(promises);
});
await this.registerAsks();
}
async registerAsks() {
global.kuzzle.onAsk("core:debugger:enable", () => this.enable());
global.kuzzle.onAsk("core:debugger:disable", () => this.disable());
global.kuzzle.onAsk("core:debugger:post", (method, params) =>
this.post(method, params)
);
global.kuzzle.onAsk("core:debugger:isEnabled", () => this.debuggerStatus);
global.kuzzle.onAsk("core:debugger:removeListener", (event, connectionId) =>
this.removeListener(event, connectionId)
);
global.kuzzle.onAsk("core:debugger:addListener", (event, connectionId) =>
this.addListener(event, connectionId)
);
}
/**
* Connect the debugger
*/
async enable() {
if (this.debuggerStatus) {
return;
}
this.inspector.connect();
this.debuggerStatus = true;
await global.kuzzle.ask("cluster:node:preventEviction", true);
}
/**
* Disconnect the debugger and clears all the events listeners
*/
async disable() {
if (!this.debuggerStatus) {
return;
}
this.inspector.disconnect();
this.debuggerStatus = false;
await global.kuzzle.ask("cluster:node:preventEviction", false);
// Disable debug mode for all connected sockets that still have listeners
if (this.httpWsProtocol) {
for (const eventName of this.events.keys()) {
for (const connectionId of this.events.get(eventName)) {
const socket =
this.httpWsProtocol.socketByConnectionId.get(connectionId);
if (socket) {
socket.internal.debugSession = false;
}
}
}
}
this.events.clear();
}
/**
* Trigger action from debugger directly following the Chrome Debug Protocol
* See: https://chromedevtools.github.io/devtools-protocol/v8/
*/
async post(method: string, params: JSONObject = {}) {
if (!this.debuggerStatus) {
throw kerror.get("core", "debugger", "not_enabled");
}
// Always disable report progress because this parameter causes a segfault.
// The reason this happens is because the inspector is running inside the same thread
// as the Kuzzle Process and reportProgress forces the inspector to call function in the JS Heap
// while it is being inspected by the HeapProfiler, which causes a segfault.
// See: https://github.com/nodejs/node/issues/44634
if (params.reportProgress) {
// We need to send a fake HeapProfiler.reportHeapSnapshotProgress event
// to the inspector to make Chrome think that the HeapProfiler is done
// otherwise, even though the Chrome Inspector did receive the whole snapshot, it will not be parsed.
//
// Chrome inspector is waiting for a HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true
// The `done` and `total` properties are only used to show a progress bar, so there are not important.
// Sending this event before the HeapProfiler.addHeapSnapshotChunk event will not cause any problem,
// in fact, Chrome always do that when taking a snapshot, it receives the HeapProfiler.reportHeapSnapshotProgress event
// before the HeapProfiler.addHeapSnapshotChunk event.
// So this will have no impact and when receiving the HeapProfiler.addHeapSnapshotChunk event, Chrome will wait to receive
// a complete snapshot before parsing it if it has received the HeapProfiler.reportHeapSnapshotProgress event with the finished property set to true before.
this.inspector.emit("inspectorNotification", {
method: "HeapProfiler.reportHeapSnapshotProgress",
params: {
done: 0,
finished: true,
total: 0,
},
});
params.reportProgress = false;
}
return this.inspectorPost(method, params);
}
/**
* Make the websocket connection listen and receive events from Chrome Debug Protocol
* See events from: https://chromedevtools.github.io/devtools-protocol/v8/
*/
async addListener(event: string, connectionId: string) {
if (!this.debuggerStatus) {
throw kerror.get("core", "debugger", "not_enabled");
}
if (this.httpWsProtocol) {
const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
if (socket) {
/**
* Mark the socket as a debugging socket
* this will bypass some limitations like the max pressure buffer size,
* which could end the connection when the debugger is sending a lot of data.
*/
socket.internal.debugSession = true;
}
}
let listeners = this.events.get(event);
if (!listeners) {
listeners = new Set();
this.events.set(event, listeners);
}
listeners.add(connectionId);
}
/**
* Remove the websocket connection from the events" listeners
*/
async removeListener(event: string, connectionId: string) {
if (!this.debuggerStatus) {
throw kerror.get("core", "debugger", "not_enabled");
}
const listeners = this.events.get(event);
if (listeners) {
listeners.delete(connectionId);
}
if (!this.httpWsProtocol) {
return;
}
const socket = this.httpWsProtocol.socketByConnectionId.get(connectionId);
if (!socket) {
return;
}
let removeDebugSessionMarker = true;
/**
* If the connection doesn't listen to any other events
* we can remove the debugSession marker
*/
for (const eventName of this.events.keys()) {
const eventListener = this.events.get(eventName);
if (eventListener && eventListener.has(connectionId)) {
removeDebugSessionMarker = false;
break;
}
}
if (removeDebugSessionMarker) {
socket.internal.debugSession = false;
}
}
/**
* Execute a method using the Chrome Debug Protocol
* @param method Chrome Debug Protocol method to execute
* @param params
* @returns
*/
private async inspectorPost(
method: string,
params: JSONObject
): Promise<JSONObject> {
if (!this.debuggerStatus) {
throw kerror.get("core", "debugger", "not_enabled");
}
let resolve;
const promise = new Promise((res) => {
resolve = res;
});
this.inspector.post(method, params, (err, res) => {
if (err) {
resolve({
error: JSON.stringify(Object.getOwnPropertyDescriptors(err)),
});
} else {
resolve(res);
}
});
return promise;
}
/**
* Sends a direct notification to a websocket connection without having to listen to a specific room
*/
private async notifyConnection(
connectionId: string,
event: string,
payload: JSONObject
) {
global.kuzzle.entryPoint._notify({
channels: [event],
connectionId,
payload,
});
}
private async notifyGlobalListeners(event: string, payload: JSONObject) {
const listeners = this.events.get("*");
if (!listeners) {
return;
}
const promises = [];
for (const connectionId of listeners) {
promises.push(
this.notifyConnection(connectionId, DEBUGGER_EVENT, {
event,
result: payload,
})
);
}
// No need to catch, notify is already try-catched
await Promise.all(promises);
}
}