forked from meteor/meteor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinspector.js
325 lines (274 loc) · 9.79 KB
/
inspector.js
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
var assert = require("assert");
var net = require("net");
var inspector = require("node-inspector");
var spawn = require("child_process").spawn;
var _ = require("underscore");
var chalk = require("chalk");
var EOL = require("os").EOL;
var Protocol = require("_debugger").Protocol;
var debugEntries = [];
// There can be only one debugger attached to a process at a time, and
// detaching can leave the child process in a weird state for future
// debugging, so the code that attaches to the child process must also
// serve as a proxy for connections from actual debugger clients like
// node-inspector.
// This proxying system requires the child process to be invoked with
// --debug-brk=<port> where <port> is not the same as debugPort, so that
// we can proxy data between <port> and debugPort, as if the child process
// were listening on debugPort (as it did before this commit).
// The first time the server starts, the --debug-brk behavior of pausing
// at the first line of the program is helpful so that the user can set
// breakpoints. When the server restarts, however, that behavior is more
// confusing than helpful, especially since the server can restart
// multiple times in quick succession if the user edits and saves a file
// multiple times. To avoid this confusion, we use the proxy to send a
// continue command to resume execution automatically after restart.
// Itercepting debugger requests, responses, events, etc. has the
// additional benefit of allowing us to print helpful information to the
// console, like notifying the developer that the debugger hit a
// breakpoint, so that there is less confusion when the app is not
// responding to requests.
function start(debugPort, entryPoint) {
debugPort = +(debugPort || 5858);
var entry = debugEntries[debugPort];
if (entry instanceof DebugEntry) {
return entry.attach;
}
debugEntries[debugPort] = entry =
new DebugEntry(debugPort, entryPoint);
return entry.attach;
}
function DebugEntry(debugPort, entryPoint) {
assert.ok(this instanceof DebugEntry);
this.debugPort = debugPort;
this.entryPoint = entryPoint;
this.incomingSink = new BackloggedStreamWriter;
this.outgoingSink = new BackloggedStreamWriter;
this.inspectorProcess = null;
this.interceptServer = null;
this.debugConnection = null;
this.connectCount = 0;
this.attach = this.attach.bind(this);
// We create a connection to whatever port the child process says it's
// listening on, so this port is purely advisory.
this.attach.suggestedDebugBrkPort = debugPort + 101;
}
var DEp = DebugEntry.prototype;
DEp.attach = function attach(child) {
this.incomingSink.clear();
this.outgoingSink.clear();
this.startInterceptServer();
this.startInspector();
this.connectToChildProcess(child);
};
// The intercept server listens for connections and data from
// node-inspector (on debugPort) and mediates communication between
// node-inspector and the child process that we're debugging, so that we
// can inject our own commands (e.g. "continue") and print helpful
// information to the console when the debugger hits breakpoints. Note
// that the intercept server survives server restarts, just like
// node-inspector.
DEp.startInterceptServer = function startInterceptServer() {
var self = this;
if (self.interceptServer) {
return;
}
self.interceptServer = net.createServer(function(socket) {
self.outgoingSink.setTarget(socket);
socket.on("data", function(buffer) {
self.incomingSink.write(buffer);
});
}).on("error", function(err) {
self.interceptServer = null;
}).listen(self.debugPort);
};
DEp.startInspector = function startInspector() {
var self = this;
if (self.inspectorProcess) {
return;
}
// Port 8080 is the default port that node-inspector uses for its web
// server, and port 5858 is the default port that node listens on when
// it receives the --debug or --debug-brk flags. Developers familiar
// with node-inspector may have http://localhost:8080/debug?port=5858
// saved in their browser history already, so let's stick with these
// conventions in the default case (unless of course the developer runs
// `meteor debug --debug-port <some other port>`).
var debugPort = self.debugPort;
var webPort = 8080 + debugPort - 5858;
var proc = spawn(process.execPath, [
require.resolve("node-inspector/bin/inspector"),
"--web-port", "" + webPort,
"--debug-port", "" + debugPort
]);
proc.url = inspector.buildInspectorUrl("localhost", webPort, debugPort);
// Forward error output to process.stderr, but silence normal output.
// proc.stdout.pipe(process.stdout);
proc.stderr.pipe(process.stderr);
proc.on("exit", function(code) {
// Restart the process if it died without us explicitly stopping it.
if (self.inspectorProcess === proc) {
self.inspectorProcess = null;
self.startInspector();
}
});
self.inspectorProcess = proc;
};
DEp.connectToChildProcess = function connectToChildProcess(child) {
var self = this;
// Wait for the child process to tell us it's listening on a certain
// port (not debugPort!), and create a connection to that port so that
// the child process can communicate with node-inspector.
child.stderr.on("data", function onData(buffer) {
var match = /debugger listening on port (\d+)/
.exec(buffer.toString("utf8"));
if (match) {
child.stderr.removeListener("data", onData);
connect(+match[1]);
}
});
function connect(port) {
disconnect();
self.debugConnection = net.createConnection(port);
self.debugConnection.setEncoding("utf8");
self.debugConnection.on("data", function(buffer) {
protocol.execute(buffer);
self.outgoingSink.write(buffer);
}).on("error", disconnect);
var protocol = new Protocol;
protocol.onResponse = function onResponse(res) {
// Listen for break events so that we can either skip them or print
// information to the console about them.
if (res.body.type === "event" &&
res.body.event === "break") {
var scriptName = res.body.body.script.name;
var lineNumber = res.body.body.sourceLine + 1;
if (self.connectCount > 1 &&
scriptName === self.entryPoint) {
// If we've restarted the server at least once and the break
// event occurred in the entry point file (typically
// .meteor/local/build/main.js), send a continue command to skip
// this breakpoint automatically, so that the user does not have
// to keep manually continuing the debugger every time the
// server restarts.
sendContinue();
} else {
// Give some indication in the console that server execution has
// stopped at a breakpoint.
process.stdout.write(
"Paused at " + scriptName + ":" + lineNumber + "\n"
);
}
}
};
var sentContinue = false;
function sendContinue() {
if (! sentContinue) {
sentContinue = true;
self.incomingSink.write(protocol.serialize({
command: "continue"
}));
}
}
if (self.connectCount++ === 0) {
process.stdout.write(banner(self.debugPort));
} else {
// Sometimes (for no good reason) the protocol.onResponse handler
// never receives a break event at the very beginning of the
// program. This timeout races against that break event to make sure
// we send exactly one continue command.
setTimeout(sendContinue, 500);
}
self.incomingSink.setTarget(self.debugConnection);
}
function disconnect() {
if (self.debugConnection) {
self.debugConnection.end();
self.debugConnection = null;
}
}
};
DEp.stop = function stop() {
var proc = this.inspectorProcess;
if (proc && proc.kill) {
this.inspectorProcess = null;
proc.kill();
}
if (this.interceptServer) {
this.interceptServer.close();
this.interceptServer = null;
}
if (this.debugConnection) {
this.debugConnection.end();
this.debugConnection = null;
}
};
// A simple wrapper object for writable streams that keeps a backlog of
// data written before the stream is available, and writes that data to the
// stream when the stream becomes available.
function BackloggedStreamWriter(target) {
assert.ok(this instanceof BackloggedStreamWriter);
this.backlog = [];
this.target = target || null;
}
var BSWp = BackloggedStreamWriter.prototype;
BSWp.write = function write(buffer) {
if (this.target) {
this.target.write(buffer);
} else {
this.backlog.push(buffer);
}
};
BSWp.setTarget = function setTarget(target) {
if (this.target &&
this.target !== target) {
this.clear();
}
this.target = target;
if (target) {
var clear = this.clear.bind(this);
target.on("close", clear);
target.on("end", clear);
if (this.backlog.length > 0) {
_.each(this.backlog.splice(0), this.write, this);
}
}
return target;
};
BSWp.clear = function clear() {
this.backlog.length = 0;
this.target = null;
};
function banner(debugPort) {
debugPort = +(debugPort || 5858);
var entry = debugEntries[debugPort];
var proc = entry && entry.inspectorProcess;
assert.strictEqual(typeof proc.url, "string");
return [
"",
chalk.green([
"Your application is now paused and ready for debugging!",
"",
"To debug the server process using a graphical debugging interface, ",
"visit this URL in your web browser:"
].join(EOL)),
chalk.cyan(proc.url),
EOL
].join(EOL);
}
function stop(debugPort) {
debugPort = +(debugPort || 5858);
var entry = debugEntries[debugPort];
delete debugEntries[debugPort];
if (entry) {
entry.stop();
}
}
require("./cleanup.js").onExit(function killAll() {
for (var debugPort in debugEntries) {
stop(debugPort);
}
debugEntries.length = 0;
});
exports.start = start;
exports.stop = stop;