first version of debugger using chrome devtools protocol

pull/483/head
nightwing 2017-10-31 22:42:48 +04:00
rodzic 6d29c6b3dc
commit d805e0322c
10 zmienionych plików z 1224 dodań i 594 usunięć

Wyświetl plik

@ -441,6 +441,10 @@ module.exports = function(options) {
packagePath: "plugins/c9.ide.run.debug/debuggers/v8/v8debugger",
basePath: workspaceDir
},
{
packagePath: "plugins/c9.ide.run.debug/debuggers/chrome/chromedebugger",
basePath: workspaceDir
},
{
packagePath: "plugins/c9.ide.run.debug/debuggers/socket",
nodeBin: nodeBin

Wyświetl plik

@ -39,7 +39,7 @@ define(function(require, exports, module) {
}
Scope.prototype = new Data(
["index", "frameIndex", "type"],
["index", "frameIndex", "type", "id"],
["variables"]
);

Wyświetl plik

@ -0,0 +1,369 @@
define(function(require, exports, module) {
"use strict";
var stream
/*global app*/
function getSocket(options, callback) {
var c9 = app.c9
var exe = c9.sourceDir + "/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy.js";
var socketPath = c9.home + "/chrome.sock";
if (c9.platform == "win32")
socketPath = "\\\\.\\pipe\\" + socketPath.replace(/\//g, "\\");
app.vfs.spawn("node", {
args: [exe],
// detached: true,
// stdio: "ignore"
stdoutEncoding: "utf8",
stderrEncoding: "utf8",
stdinEncoding: "utf8",
}, function(err, meta) {
console.log(err, meta)
meta.process.stdout.on("data", function(e) {console.log(e)})
meta.process.stderr.on("data", function(e) {console.log(e)})
meta.process.on("exit", function(e) {console.log(e)})
tryConnect(10, connectPort)
})
function tryConnect(retries, cb) {
cb(function next(err) {
if (err) {
return setTimeout(function() {
tryConnect(retries-1, cb)
}, 100)
}
cb()
});
}
function connectPort(cb) {
app.net.connect(socketPath, {}, function(err, s) {
if (err) return cb(err)
stream = s;
var buff = [];
stream.on("data", function(data) {
var idx;
while (true) {
idx = data.indexOf("\0");
if (idx === -1)
return data && buff.push(data);
buff.push(data.substring(0, idx));
var clientMsg = buff.join("");
data = data.substring(idx + 1);
buff = [];
var m;
try {
m = JSON.parse(clientMsg);
} catch (e) {
continue;
}
socket.emit("message", m);
}
});
// Don't call end because session will remain in between disconnects
stream.on("end", function(err) {
console.log("end", err);
socket.emit("end", err);
});
stream.on("error", function(err) {
socket.emit("error", err);
});
socket.send({ $: "connect", port: options.port, host: options.host });
socket.on("message", function me(m) {
if (m && m.$ == "connected") {
socket.off("message", me);
callback(null, socket);
}
});
});
}
var socket = Object.create(EventEmitter);
socket.emit = socket._signal;
socket.send = function(s) {
stream.write(JSON.stringify(s) + "\0");
};
socket.close = function() {
stream.end();
};
}
var oop = require("ace/lib/oop");
var EventEmitter = require("ace/lib/event_emitter").EventEmitter;
var DevtoolsProtocol = module.exports = function() {
this.callbacks = {};
this.$scripts = {};
};
(function() {
oop.implement(this, EventEmitter);
this.events = [
"changeRunning",
"break",
"exception",
"afterCompile"
];
this.$seq = 0;
this.$send = function(method, params, callback) {
this.$seq++;
if (callback)
this.callbacks[this.$seq] = callback;
this.ws.send({
id: this.$seq,
method: method,
params: params || undefined,
});
};
this.handleMessage = function(message) {
if (message.id) {
if (this.callbacks[message.id])
return this.callbacks[message.id](message.result, message.error);
} else {
var params = message.params;
if (message.method == "Debugger.scriptParsed") {
this.$scripts[params.scriptId] = params;
this._signal("afterCompile", params);
}
else if (message.method == "Runtime.executionContextCreated") {
console.log(message.params);
}
else if (message.method == "Runtime.executionContextDestroyed") {
console.log(message.params);
this.detachDebugger();
}
else if (message.method == "Debugger.resumed") {
this.$callstack = null;
this._signal("changeRunning", params);
console.warn(message);
}
else if (message.method == "Debugger.paused") {
this.$callstack = params;
this._signal("changeRunning", params);
console.warn(message);
if (params.reason == "exception") {
this._signal("exception", params);
} else {
this._signal("break", params);
}
}
else {
console.warn(message);
}
}
};
this.detachDebugger = function() {
if (this.ws)
this.ws.send({ $: "detach" });
}
this.attach = function(port, cb) {
var that = this;
getSocket({
host: "127.0.0.1",
port: port
}, function(err, ws) {
that.ws = ws;
that.ws.on("message", that.handleMessage.bind(that));
that.$send("Profiler.enable");
that.$send("Runtime.enable");
that.$send("Debugger.enable");
// that.$send("Debugger.setPauseOnExceptions", {"state":"uncaught"});
// that.$send("Debugger.setBlackboxPatterns", {"patterns":[]});
that.$send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
that.$send("Runtime.runIfWaitingForDebugger");
cb();
});
};
this.detach = function() {
if (this.ws)
this.ws.close();
};
this.isRunning = function() {
return !this.$callstack;
};
this.stepInto = function(callback) {
this.$send("Debugger.stepInto", null, callback);
};
this.stepOver = function(callback) {
this.$send("Debugger.stepOver", null, callback);
};
this.stepOut = function(callback) {
this.$send("Debugger.stepOut", null, callback);
};
this.resume = function(callback) {
this.$send("Debugger.resume", null, callback);
};
this.suspend = function(callback) {
this.$send("Debugger.pause", null, callback);
};
this.backtrace = function(callback) {
callback(this.$callstack || []);
};
this.getProperties = function(params, callback) {
this.$send("Runtime.getProperties", params, callback);
};
this.scripts = function(callback) {
callback(this.$scripts);
};
this.getScriptSource = function(id, callback) {
this.$send("Debugger.getScriptSource", { scriptId: id }, callback);
};
this.evaluate = function(expression, frame, global, disableBreak, callback) {
if (frame) {
this.$send("Debugger.evaluateOnCallFrame", {
expression: expression,
callFrameId: frame.id,
objectGroup: "popover",
includeCommandLineAPI: false,
silent: true,
returnByValue: false,
generatePreview: false,
}, callback);
} else {
this.$send("Runtime.evaluate", {
expression: expression,
objectGroup: "console",
includeCommandLineAPI: true,
silent: false,
contextId: 1,
returnByValue: false,
generatePreview: true,
userGesture: true,
awaitPromise: false,
}, callback);
}
};
this.setexceptionbreak = function(state, callback) {
this.$send("Debugger.setPauseOnExceptions", {
state: state
}, callback);
};
this.setvariablevalue = function(variable, value, frame, callback) {
if (!variable.parent)
return;
this.evaluate("(" + value + ")", frame, null, true, function(data, err) {
if (err)
return callback(err);
if (variable.parent.index != null) {
this.$send("Debugger.setVariableValue", {
scopeNumber: variable.parent.index,
variableName: variable.name,
newValue: data.result,
callFrameId: frame.id,
}, function(data, err) {
callback(err);
});
}
else {
this.$send("Runtime.callFunctionOn", {
"objectId": variable.parent.ref || variable.parent.id,
"functionDeclaration": "function(a, b) { this[a] = b; }",
"arguments": [
{ "value": variable.name },
data.result
],
"silent": true
}, function(data, err) {
callback(err);
});
}
}.bind(this));
};
this.setbreakpoint = function(target, line, column, enabled, condition, callback) {
// lineNumber| columnNumber
// url | urlRegex | scriptHash
// condition
var breakpointId = target + ":" + line + ":" + column;
this.$send("Debugger.removeBreakpoint", {
breakpointId: breakpointId
}, function() {
if (!enabled) callback(null, {});
});
if (!enabled) return;
this.$send("Debugger.setBreakpointByUrl", {
lineNumber: line,
url: target,
// urlRegex:
columnNumber: column || 0,
condition: condition
}, function(info) {
callback(info);
});
};
this.clearbreakpoint = function(breakpointId, callback) {
this.$send("Debugger.removeBreakpoint", {
breakpointId: breakpointId
}, callback);
};
this.listbreakpoints = function(callback) {
callback({
breakpoints: []
});
};
this.changelive = function(scriptId, newSource, previewOnly, callback, $retry) {
var that = this;
that.$send("Debugger.setScriptSource", {
scriptId: scriptId,
scriptSource: newSource,
dryRun: !!previewOnly
}, function(result, error) {
if (error && error.code == -32000 && !$retry) {
return that.changelive(scriptId, newSource, previewOnly, callback, true);
}
if (result && result.stackChanged) {
return that.stepInto(function() {
callback({}, null);
});
}
callback(result, error);
});
};
this.restartframe = function(frameId, callback) {
this.$send("Debugger.restartFrame", {
callFrameId: frameId
}, callback);
};
// TODO add support for this in debugger
// this.disableBreak = function() {
// "Debugger.setBreakpointsActive"
// "Debugger.setSkipAllPauses"
// };
}).call(DevtoolsProtocol.prototype);
});

Wyświetl plik

@ -0,0 +1,17 @@
define(function(require, exports, module) {
module.exports = function(vfs, options) {
vfs.extend("chromeDebugProxyLauncher", {
code: options.standalone ? undefined : require("text!./bridge-service.js"),
file: options.standalone ? "c9.cli.bridge/bridge-service.js" : undefined,
redefine: true
}, function(err, remote) {
});
};
});

Wyświetl plik

@ -0,0 +1,348 @@
/*
ide1 ide2
| |
vfsServer1 vfsServer2 vfsServer3 ... clients
| |
| | filesocket
| |
chrome-debug-proxy
|
| websocket
|
node-process1 node-process2 ... debuggers
*/
var fs = require("fs");
var net = require("net");
var WebSocket = require("ws");
var startT = Date.now();
/*** helpers ***/
/*** connect to cloud9 ***/
var socketPath = process.env.HOME + "/chrome.sock";
if (process.platform == "win32")
socketPath = "\\\\.\\pipe\\" + socketPath.replace(/\//g, "\\");
console.log(socketPath);
function checkServer() {
var currentT
var client = net.connect(socketPath, function() {
console.log("process already exists");
// process.exit(0);
});
fs.stat(__filename, function(err, stat) {
currentT = stat ? stat.mtime.valueOf() : 0;
console.log(currentT);
// client.send({ $: "exit" });
});
client.on("data", function(data) {
var m = JSON.parse(data.slice(0, -1));
console.log(data + "");
if (m.$ == "refresh" && m.t < currentT)
client.write(JSON.stringify({ $: "exit" }) + "\0");
});
client.on("error", function(err) {
if (err && (err.code === "ECONNREFUSED" || err.code === "ENOENT" || err.code === "EAGAIN")) {
createServer();
}
else {
process.exit(1);
}
});
}
var ideClients = {};
var counter = 0;
var server;
function createServer() {
server = net.createServer(function(client) {
var isClosed = false;
client.id = counter++;
ideClients[client.id] = client;
client.send = function(msg) {
if (isClosed)
return;
var strMsg = JSON.stringify(msg);
client.write(strMsg + "\0");
};
client.on("data", onData);
var buff = [];
function onData(data) {
data = data.toString();
var idx;
while (true) {
idx = data.indexOf("\0");
if (idx === -1)
return data && buff.push(data);
buff.push(data.substring(0, idx));
var clientMsg = buff.join("");
data = data.substring(idx + 1);
buff = [];
client.emit("message", JSON.parse(clientMsg));
}
}
client.on("close", onClose);
client.on("end", onClose);
client.on("message", function(message) {
console.log(message);
if (actions[message.$])
actions[message.$](message, client);
else if (client.debugger)
client.debugger.handleMessage(message);
});
function onClose() {
if (isClosed) return;
isClosed = true;
delete ideClients[client.id];
if (client.debugger)
client.debugger.removeClient(client);
client.emit("disconnect");
}
client.on("error", function(err) {
console.log(err);
onClose();
client.destroy();
});
client.send({ $: "refresh", t: startT });
});
server.on("error", function(e) {
console.log(e);
console.log("+++++++++++++++++++++++++++");
process.exit(1);
});
if ((process.platform == "win32")) {
server.listen(socketPath, function() {
console.log("---------------------------");
});
}
else {
fs.unlink(socketPath, function(e) {
server.listen(socketPath, function() {
console.log("---------------------------");
});
});
}
}
var actions = {
exit: function(message, client) {
process.exit(1);
},
ping: function(message, client) {
message.$ = "pong";
message.t = Date.now();
client.send(message);
},
connect: function(message, client, callback) {
// if (!debuggers[message.port]) {
debuggers[message.port] = new Debugger();
debuggers[message.port].connect(message);
// }
debuggers[message.port].addClient(client);
},
detach: function(message, client, callback) {
if (client.debugger)
client.debugger.disconnect();
},
};
/*** connect to node ***/
function Debugger(options) {
this.clients = [];
}
(function() {
this.addClient = function(client) {
this.clients.push(client);
// client.send({$: 1});
client.debugger = this;
};
this.removeClient = function(client) {
var i = this.clients.indexOf(client);
if (i != -1)
this.clients.splice(i, 1);
client.debugger = null;
};
this.handleMessage = function(message) {
console.log(">>" + JSON.stringify(message))
if (this.ws)
this.ws.send(JSON.stringify(message));
else
console.log(message);
};
this.connect = function(options) {
getDebuggerData(options.port, function(err, res) {
if (err) console.log(err) //TODO
var header = res[0];
var tabs = res[1];
if (!tabs) {
return // old debugger
}
if (tabs.length > 1)
console.log("===========================");
if (tabs[0] && tabs[0].webSocketDebuggerUrl) {
this.connectToWebsocket(tabs[0].webSocketDebuggerUrl);
}
}.bind(this));
};
this.connectToWebsocket = function(url) {
var clients = this.clients;
function broadcast(message) {
if (typeof message !== "string")
message = JSON.stringify(message);
clients.forEach(function(c) {
console.log(c.id, "[][]");
c.write(message + "\0");
});
}
var ws = new WebSocket(url);
ws.on("open", function open() {
console.log("connected");
broadcast({ $: "connected" });
});
ws.on("close", function close() {
console.log("disconnected");
});
ws.on("message", function incoming(data) {
console.log("<<" + data);
broadcast(data);
});
ws.on("error", function(e) {
console.log("error", e);
broadcast({ $: "error", err: e });
});
this.ws = ws;
};
this.disconnect = function() {
if (this.ws)
this.ws.close();
this.clients.forEach(function(client) {
client.end();
});
};
}).call(Debugger.prototype);
var RETRY_INTERVAL = 300;
var MAX_RETRIES = 100;
var debuggers = {};
function getDebuggerData(port, callback, retries) {
console.log("Connecting to port", port, retries);
if (retries == null) retries = MAX_RETRIES;
request({
host: "127.0.0.1",
port: port,
path: "/json/list",
}, function(err, res) {
if (err && retries > 0) {
return setTimeout(function() {
getDebuggerData(port, callback, retries - 1);
}, RETRY_INTERVAL);
}
console.log(res);
callback(err, res);
});
}
function request(options, cb) {
var socket = new net.Socket();
var received = "";
var expectedBytes = 0;
var offset = 0;
function readBytes(str, start, bytes) {
// returns the byte length of an utf8 string
var consumed = 0;
for (var i = start; i < str.length; i++) {
var code = str.charCodeAt(i);
if (code < 0x7f) consumed++;
else if (code > 0x7f && code <= 0x7ff) consumed += 2;
else if (code > 0x7ff && code <= 0xffff) consumed += 3;
if (code >= 0xD800 && code <= 0xDBFF) i++; // leading surrogate
if (consumed >= bytes) { i++; break; }
}
return { bytes: consumed, length: i - start };
}
function parse(data) {
var fullResponse = false;
received += data;
if (!expectedBytes) { // header
var i = received.indexOf("\r\n\r\n");
if (i !== -1) {
var c = received.lastIndexOf("Content-Length:", i);
if (c != -1) {
var l = received.indexOf("\r\n", c);
var len = parseInt(received.substring(c + 15, l), 10);
expectedBytes = len;
}
offset = i + 4;
}
}
if (expectedBytes) { // body
var result = readBytes(received, offset, expectedBytes);
expectedBytes -= result.bytes;
offset += result.length;
}
if (offset && expectedBytes <= 0) {
fullResponse = received.substring(0, offset);
received = received.substr(offset);
offset = expectedBytes = 0;
}
return fullResponse && fullResponse.split("\r\n\r\n");
}
socket.on("data", function(data) {
console.log(data + "")
var response = parse(data);
if (response) {
socket.end();
if (response[1]) {
try {
response[1] = JSON.parse(response[1]);
} catch (e) {}
}
cb(null, response);
}
});
socket.on("error", function(e) {
console.log("==~==", e)
socket.end();
cb(e);
});
socket.connect(options.port, options.host);
socket.on("connect", function() {
console.log("~==")
socket.write("GET " + options.path + " HTTP/1.1\r\nConnection: close\r\n\r\n");
});
}
/*** =============== ***/
checkServer();
setInterval(function() {
console.log(Date.now());
}, 60000);

Wyświetl plik

@ -0,0 +1,84 @@
"use strict";
"use server";
require("c9/inline-mocha")(module);
require("amd-loader");
var childProcess = require("child_process");
var fs = require("fs");
var net = require("net");
var socketPath = process.env.HOME + "/chrome.sock";
if (process.platform == "win32")
socketPath = "\\\\.\\pipe\\" + socketPath.replace(/\//g, "\\");
process.chdir(__dirname);
function debuggerProxy(id, handlers) {
var p1 = childProcess.spawn(process.execPath, ["./chrome-debug-proxy.js"]);
p1.stdout.once("data", function(data) {
handlers.onStart && handlers.onStart();
});
p1.stdout.on("data", function(data) {
console.log(id, data + "");
});
p1.stderr.on("data", function(data) {
console.log(id, data + "");
});
p1.on("close", function(code) {
console.log(id, code);
});
p1.on("exit", function(code) {
handlers.onExit && handlers.onExit(code);
console.log(id, code);
});
p1.on("error", function(code) {
console.log(id, code);
});
return {
exit: p1.kill.bind(p1),
};
}
describe(__filename, function() {
var p1, p2, p3;
this.timeout(10000);
it("should exit if another server is running", function(done) {
try {
fs.unlinkSync(socketPath);
} catch (e) {}
p1 = debuggerProxy("p1", {
onStart: function() {
},
onExit: function() {
}
});
p2 = debuggerProxy("p2", {
onExit: function() {
done();
}
});
});
it("should connect to node", function(done) {
p3 = childProcess.spawn(process.execPath, [
"--inspect=58974", "-e", "setTimeout(x=>x, 10000)"
], { stdio: "inherit" });
var client = net.connect(socketPath, function() {
console.log("=====");
client.on("data", function handShake(data) {
// logVerbose("[vfs-collab]", "Client handshaked", data.toString());
console.log("=====" + data);
});
client.write(JSON.stringify({ m: "ping" }) + "\0");
client.write(JSON.stringify({ m: "connect", port: 58974 }) + "\0");
});
});
after(function() {
p1 && p1.kill();
p2 && p2.kill();
p3 && p3.kill();
});
});

Wyświetl plik

@ -24,6 +24,9 @@ define(function(require, exports, module) {
var emit = socket.getEmitter();
var state, stream, connected, away;
if (proxy == false)
return socket;
if (typeof proxy == "string")
proxy = { source: proxy };

Wyświetl plik

@ -1,5 +1,18 @@
{
"cmd": ["bash", "--login", "-c", "nvm use default > /dev/null; node ${debug?--nocrankshaft --nolazy --nodead_code_elimination --debug-brk=15454} '$file' $args"],
"script": [
"set -e",
"if ! [ \"$debug\" == true ]; then ",
" node $file $args",
"elif node --debug -e '' &> /dev/null; then",
" FLAGS=\"--nocrankshaft --nolazy --debug-brk=$debugport\"",
" if node --nodead_code_elimination -e '' &> /dev/null; then",
" FLAGS=\"$FLAGS --nodead_code_elimination\"",
" fi",
" node $FLAGS $file $args",
"else",
" node --inspect-brk=$debugport $file $args",
"fi"
],
"debugger": "v8",
"debugport": 15454,
"selector": "source.js",

Wyświetl plik

@ -1,12 +1,17 @@
{
"cmd": [
"node",
"${debug?--nocrankshaft}",
"${debug?--nolazy}",
"${debug?`node --version | grep -vqE \"v0\\..\\.\" && echo --nodead_code_elimination`}",
"${debug?--debug-brk=$debugport}",
"$file",
"$args"
"script": [
"set -e",
"if ! [ \"$debug\" == true ]; then ",
" node $file $args",
"elif node --debug -e '' &> /dev/null; then",
" FLAGS=\"--nocrankshaft --nolazy --debug-brk=$debugport\"",
" if node --nodead_code_elimination -e '' &> /dev/null; then",
" FLAGS=\"$FLAGS --nodead_code_elimination\"",
" fi",
" node $FLAGS $file $args",
"else",
" node --inspect-brk=$debugport $file $args",
"fi"
],
"debugger": "v8",
"debugport": 15454,