diff --git a/configs/ide/default.js b/configs/ide/default.js index cb53d374..ca742c09 100644 --- a/configs/ide/default.js +++ b/configs/ide/default.js @@ -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 diff --git a/plugins/c9.ide.run.debug/data/scope.js b/plugins/c9.ide.run.debug/data/scope.js index 7e9b2542..b83b2201 100644 --- a/plugins/c9.ide.run.debug/data/scope.js +++ b/plugins/c9.ide.run.debug/data/scope.js @@ -39,7 +39,7 @@ define(function(require, exports, module) { } Scope.prototype = new Data( - ["index", "frameIndex", "type"], + ["index", "frameIndex", "type", "id"], ["variables"] ); diff --git a/plugins/c9.ide.run.debug/debuggers/chrome/DevtoolsProtocol.js b/plugins/c9.ide.run.debug/debuggers/chrome/DevtoolsProtocol.js new file mode 100644 index 00000000..2033433a --- /dev/null +++ b/plugins/c9.ide.run.debug/debuggers/chrome/DevtoolsProtocol.js @@ -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); + + +}); \ No newline at end of file diff --git a/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy-launcher.js b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy-launcher.js new file mode 100644 index 00000000..4c44c782 --- /dev/null +++ b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy-launcher.js @@ -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) { + + }); + }; + + + + + +}); \ No newline at end of file diff --git a/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy.js b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy.js new file mode 100644 index 00000000..6d0178d3 --- /dev/null +++ b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy.js @@ -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); \ No newline at end of file diff --git a/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy_test.js b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy_test.js new file mode 100644 index 00000000..dee29434 --- /dev/null +++ b/plugins/c9.ide.run.debug/debuggers/chrome/chrome-debug-proxy_test.js @@ -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(); + }); +}); diff --git a/plugins/c9.ide.run.debug/debuggers/chrome/chromedebugger.js b/plugins/c9.ide.run.debug/debuggers/chrome/chromedebugger.js index 0b87f2c5..56e48600 100644 --- a/plugins/c9.ide.run.debug/debuggers/chrome/chromedebugger.js +++ b/plugins/c9.ide.run.debug/debuggers/chrome/chromedebugger.js @@ -1,19 +1,3 @@ -/** - * node debugger Module for the Cloud9 - * - * @copyright 2013, Ajax.org B.V. - */ - -//https://coderwall.com/p/hkmedw?utm_campaign=weekly_digest&utm_content=2013-04-02+00%3A00%3A00+UTC&utm_medium=email -//https://github.com/johnjbarton/chrome.debugger.remote -//https://github.com/cyrus-and/chrome-remote-interface -//https://github.com/danielconnor/node-devtools/tree/master/lib - -//https://github.com/cyrus-and/chrome-remote-interface -//https://developers.google.com/chrome-developer-tools/docs/protocol/1.0/debugger#event-paused -//https://github.com/google/crx2app/issues/1 -//https://github.com/google/devtoolsExtended - define(function(require, exports, module) { main.consumes = ["Plugin", "debugger", "util", "c9"]; main.provides = ["chromedebugger"]; @@ -24,26 +8,31 @@ define(function(require, exports, module) { var util = imports.util; var debug = imports["debugger"]; var c9 = imports.c9; + var async = require("async"); - var Frame = require("../../data/frame"); - var Source = require("../../data/source"); - var Breakpoint = require("../../data/breakpoint"); - var Variable = require("../../data/variable"); - var Scope = require("../../data/scope"); + var Frame = debug.Frame; + var Source = debug.Source; + var Breakpoint = debug.Breakpoint; + var Variable = debug.Variable; + var Scope = debug.Scope; - var V8Debugger = require("../v8/lib/V8Debugger"); - var V8DebuggerService = require("../v8/lib/StandaloneV8DebuggerService"); - + var DevtoolsProtocol = require("./DevtoolsProtocol"); + /* + - activate/deactivate all breakpoints + - reconnect + - break at start + - conflicting state at onchangerunning + - + */ /***** Initialization *****/ var plugin = new Plugin("Ajax.org", main.consumes); var emit = plugin.getEmitter(); emit.setMaxListeners(1000); - var stripPrefix = options.basePath || ""; + var stripPrefix = c9.toInternalPath((options.basePath || "").replace(/[\/\\]$/, "")); var breakOnExceptions = false; var breakOnUncaughtExceptions = false; - var breakpointQueue = []; var NODE_PREFIX = "(function (exports, require, module, __filename, __dirname) { "; var NODE_POSTFIX = "\n});"; @@ -51,18 +40,10 @@ define(function(require, exports, module) { var RE_NODE_PREFIX = new RegExp("^" + util.escapeRegExp(NODE_PREFIX)); var RE_NODE_POSTFIX = new RegExp(util.escapeRegExp(NODE_POSTFIX) + "$"); - var TYPE = "v8"; + var TYPE = "chrome"; var attached = false; - var v8dbg, v8ds, state, activeFrame, sources, socket; - - var scopeTypes = { - "0": "global", - "1": "local", - "2": "with", - "3": "function", - "4": "catch" - }; + var v8dbg, state, activeFrame, sources, socket, pathMap; var hasChildren = { "regexp": 32, @@ -71,19 +52,6 @@ define(function(require, exports, module) { "function": 4 }; - var loaded = false; - function load() { - if (loaded) return false; - loaded = true; - - debug.registerDebugger(TYPE, plugin); - } - - function unload() { - debug.unregisterDebugger(TYPE, plugin); - loaded = false; - } - /***** Helper Functions *****/ /** @@ -92,36 +60,20 @@ define(function(require, exports, module) { function sync(breakpoints, reconnect, callback) { if (!v8dbg) return console.error("Sync called without v8dbg"); - getSources(function(err, sources) { if (err) return callback(err); - getFrames(function(err, frames) { + updateBreakpoints(breakpoints, reconnect, function(err, breakpoints) { if (err) return callback(err); - updateBreakpoints(breakpoints, reconnect, function(err, breakpoints) { - if (err) return callback(err); - - handleDebugBreak(breakpoints, reconnect, frames[0], function(canAttach) { - attached = canAttach; - emit("attach", { breakpoints: breakpoints }); - }, - function(isResumed) { - // This check is for when the process is not - // started with debug-brk - if (activeFrame) { - onChangeFrame(activeFrame); - emit("break", { - frame: activeFrame, - frames: frames - }); - } - - onChangeRunning(null, isResumed); - callback(); - }); + handleDebugBreak(breakpoints, reconnect, null, function(canAttach) { + attached = canAttach; + emit("attach", { breakpoints: breakpoints }); + }, + function(isResumed) { + callback(); }); - }, true); // The sync backtrace should be silent + }); }); } @@ -139,14 +91,6 @@ define(function(require, exports, module) { listBreakpoints(function handleBps(err, remoteBreakpoints) { if (err) return callback(err); - // We should always have at least 1 breakpoint - if (!reconnect && !remoteBreakpoints.length && ++retries < 10) { - setTimeout(function() { - if (v8dbg) listBreakpoints(handleBps); - }, 100); - return; - } - var found = []; var notfound = []; @@ -160,18 +104,12 @@ define(function(require, exports, module) { notfound.push(rbp); }); - var i = 0; - function next() { - var bp = list[i++]; - if (!bp) - done(); - else if (found.indexOf(bp) == -1) + async.each(list, function(bp, next) { + if (found.indexOf(bp) == -1) setBreakpoint(bp, next); else next(); - } - - next(); + }, done); function done() { notfound.forEach(function(bp) { @@ -201,67 +139,30 @@ define(function(require, exports, module) { return callback(); } - var bp = breakpoints[0]; - - // If there's no breakpoint set - if (!bp) { - attach(reconnect || 0); - - // If we reconnect to a break then don't resume. - if (reconnect) { - onChangeFrame(frame); - callback(); - } - else - resume(callback.bind(this, true)); - - return; - } - - // Check for a serverOnly breakpoint on line 0 - // this bp, is automatically created by v8 to stop on break - if (bp.id === 1 && bp.serverOnly && bp.line === 0) { - // The breakpoint did it's job, now lets remove it - v8dbg.clearbreakpoint(1, wait); - breakpoints.remove(bp); - } - else wait(); + wait(); + reconnect = true; function wait() { // Check if there is a real breakpoint here, so we don't resume - function checkEval(err, variable) { - if (err || isTruthy(variable)) { - onChangeFrame(null); - attach(true); - resume(callback.bind(this, true)); + function resumeIfNeeded(callback) { + if (!activeFrame) { + return v8dbg.$waitForBreak = function(frame) { + activeFrame = frame; + v8dbg.$waitForBreak = null; + resumeIfNeeded(function() { + callback(); + }); + return true; + }; } - else { - onChangeFrame(frame); - attach(true); - callback(false); - } - } - - // @todo this is probably a timing issue - probably solved now - if (frame) { - var test = { path: frame.path, line: frame.line }; - for (var bpi, i = 0, l = breakpoints.length; i < l; i++) { - if ((bpi = breakpoints[i]).equals(test)) { - // If it's not enabled let's continue - if (!bpi.enabled) - break; - - // Check a condition if it has it - if (bpi.condition) { - evaluate(bpi.condition, frame, false, true, checkEval); - } - else { - onChangeFrame(frame); - attach(true); - callback(false); - } - return; - } + var onBreakpoint = breakpoints.some(function(bp) { + return bp.enabled && activeFrame.path === bp.path && bp.line == activeFrame.line; + }); + if (!onBreakpoint) { + resume(callback); + } else { + onChangeRunning(); + onBreak({frame: activeFrame}); } } @@ -272,9 +173,9 @@ define(function(require, exports, module) { callback(false); } else { - onChangeFrame(null); + // onChangeFrame(null); attach(true); - resume(callback.bind(this, true)); + resumeIfNeeded(callback.bind(this, true)); } } } @@ -283,9 +184,12 @@ define(function(require, exports, module) { * Removes the path prefix from a string */ function strip(str) { + if (!str) return ""; + str = c9.toInternalPath(str); + str = applyPathMap(str, "toInternal"); return str && str.lastIndexOf(stripPrefix, 0) === 0 - ? str.slice(stripPrefix.length) - : str || ""; + ? util.normalizePath(str.slice(stripPrefix.length)) + : util.normalizePath(str || ""); } /** @@ -301,6 +205,8 @@ define(function(require, exports, module) { } function formatType(value) { + if (value.description) + return value.description; switch (value.type) { case "undefined": case "null": @@ -342,21 +248,6 @@ define(function(require, exports, module) { return true; } - function frameToString(frame) { - var str = []; - var args = frame.arguments; - var argsStr = []; - - str.push(frame.func.name || frame.func.inferredName || "anonymous", "("); - for (var i = 0, l = args.length; i < l; i++) { - var arg = args[i]; - if (!arg.name) - continue; - argsStr.push(arg.name); - } - str.push(argsStr.join(", "), ")"); - return str.join(""); - } function getPathFromScriptId(scriptId) { for (var i = 0; i < sources.length; i++) { @@ -373,50 +264,30 @@ define(function(require, exports, module) { } function getLocalScriptPath(script) { - var scriptName = script.name || ("-anonymous-" + script.id); - if (stripPrefix == "/") { - if (c9.platform == "win32" && scriptName[1] == ":") - scriptName = "/" + scriptName; - } else if (scriptName.substring(0, stripPrefix.length) == stripPrefix) - scriptName = scriptName.substr(stripPrefix.length); - - // windows paths come here independantly from vfs - return scriptName.replace(/\\/g, "/"); + var scriptName = script.url || ("-anonymous-" + script.scriptId); + scriptName = c9.toExternalPath(scriptName); + scriptName = strip(scriptName); + return scriptName; } - function createFrame(options, script) { - var frame = new Frame({ - index: options.index, - name: apf.escapeXML(frameToString(options)), //dual escape??? - column: options.column, - id: getFrameId(options), - line: options.line, - script: strip(script.name), - path: getLocalScriptPath(script), - sourceId: options.func.scriptId + function createFrame(frameData, index) { + var options = frameData[index]; + var frame = options._frame || new Frame({ + istop: index == 0, + index: index, + name: options.functionName || "(anonymous)", + line: options.location.lineNumber, + column: options.location.columnNumber, + id: options.callFrameId, + script: options.location.scriptId, + path: getLocalScriptPath(v8dbg.$scripts[options.location.scriptId]), + sourceId: options.location.scriptId }); + options._frame = frame; var vars = []; - - // Arguments - options.arguments.forEach(function(arg) { - vars.push(createVariable(arg, null, "arguments")); - }); - - // Local variables - options.locals.forEach(function(local) { - if (local.name !== ".arguments") - vars.push(createVariable(local, null, "locals")); - }); - - // Adding the local object as this - vars.push(createVariable({ - name: "this", - value: options.receiver, - kind: "this" - })); - - frame.variables = vars; + + frame.variables = frame.variables || vars; /* 0: Global @@ -426,15 +297,16 @@ define(function(require, exports, module) { 4: Catch >, if (scope.type > 1) {*/ - frame.scopes = options.scopes.filter(function(scope) { - return scope.type != 1; - }).reverse().map(function(scope) { + frame.scopes = options.scopeChain.map(function(scope, i) { return new Scope({ - index: scope.index, - type: scopeTypes[scope.type], - frameIndex: frame.index + id: scope.object.objectId, + type: scope.type, + frame: frame, + index: i }); }); + if (frame.scopes[0]) + frame.scopes[0].isOpen = true; return frame; } @@ -443,8 +315,8 @@ define(function(require, exports, module) { var value = options.value || options; if (variable) { - variable.value = formatType(options); - variable.type = options.type; + // variable.value = formatType(options); + // variable.type = options.type; } else { variable = new Variable({ @@ -452,52 +324,52 @@ define(function(require, exports, module) { scope: scope, value: formatType(value), type: value.type, - ref: typeof value.ref == "number" - ? value.ref - : value.handle, + ref: value.objectId, children: options.children === false - ? false : (hasChildren[value.type] ? true : false) + ? false : (hasChildren[value.type] ? true : false), + options: options, }); } - if (value.prototypeObject) - variable.prototype = new Variable({ - tagName: "prototype", - name: "prototype", - type: "object", - ref: value.prototypeObject.ref - }); - if (value.protoObject) - variable.proto = new Variable({ - tagName: "proto", - name: "proto", - type: "object", - ref: value.protoObject.ref - }); - if (value.constructorFunction) - variable.constructorFunction = new Variable({ - tagName: "constructor", - name: "constructor", - type: "function", - ref: value.constructorFunction.ref - }); + // if (value.prototypeObject) + // variable.prototype = new Variable({ + // tagName: "prototype", + // name: "prototype", + // type: "object", + // ref: value.prototypeObject.ref + // }); + // if (value.protoObject) + // variable.proto = new Variable({ + // tagName: "proto", + // name: "proto", + // type: "object", + // ref: value.protoObject.ref + // }); + // if (value.constructorFunction) + // variable.constructorFunction = new Variable({ + // tagName: "constructor", + // name: "constructor", + // type: "function", + // ref: value.constructorFunction.ref + // }); return variable; } - function updateVariable(variable, body) { - return createVariable(body, null, null, variable); - } - function createSource(options) { var path = getLocalScriptPath(options); + // isLiveEdit: false, + // sourceMapURL: "", + // hasSourceURL: false, + // isModule: false, return new Source({ - id: options.id, - name: options.name || "anonymous", + id: options.scriptId, + name: options.url || "anonymous", path: path, text: strip(options.text || "anonymous"), debug: path.charAt(0) != "/" || path.match(/ \(old\)$/) ? true : false, lineOffset: options.lineOffset, - customSyntax: "javascript" + customSyntax: "javascript", + threadId: options.executionContextId, }); } @@ -520,83 +392,54 @@ define(function(require, exports, module) { if (!v8dbg) { state = null; } else { - state = v8dbg.isRunning() || isResumed ? "running" : "stopped"; + state = v8dbg.isRunning() /*|| isResumed*/ ? "running" : "stopped"; } - - emit("stateChange", { state: state }); - - if (state != "stopped") - onChangeFrame(null); - } + + if (attached) { + emit("stateChange", { state: state }); - function createFrameFromBreak(data) { - // Create a frame from the even information - return new Frame({ - index: 0, - name: data.invocationText, - column: data.sourceColumn, - id: String(data.line) + ":" + String(data.sourceColumn), - line: data.sourceLine, - script: strip(data.script.name), - path: getLocalScriptPath(data.script), - sourceId: data.script.id, - istop: true - }); + if (state != "stopped") + onChangeFrame(null); + } } function onBreak(e) { - if (!attached) { - if (attached === 0) - attached = true; + var frame = e.frame || createFrame(e.callFrames, 0); + if (v8dbg.$waitForBreak && v8dbg.$waitForBreak(frame)) { return; } - - // @todo update breakpoint text? - - var frame = createFrameFromBreak(e.data); - onChangeFrame(frame); - emit("break", { - frame: frame - }); + onChangeFrame(frame, !attached); + if (attached) + emit("break", { frame: frame }); } function onException(e) { - var frame = createFrameFromBreak(e.data); + var frame = createFrame(e.callFrames, 0); - var options = e.data.exception; - options.text.match(/^(\w+):(.*)$/); - var name = RegExp.$1 || options.className; - var value = RegExp.$2 || options.text; + var options = e.data; + var m = /^(\w+):(.*)$/m.exec(options.description); + var name = m && m[1] || options.className; + var value = m && m[2] || options.description; options.name = name; options.value = { value: value, - type: "error", - handle: options.handle + type: "error", + objectId: options.objectId, }; options.children = true; var variable = createVariable(options); variable.error = true; - lookup(options.properties, false, function(err, properties) { - variable.properties = properties; - - emit("exception", { - frame: frame, - exception: variable - }); + emit("exception", { + frame: frame, + exception: variable }); } function onAfterCompile(e) { - var queue = breakpointQueue; - breakpointQueue = []; - queue.forEach(function(i) { - setBreakpoint(i[0]); - }); - - emit("sourcesCompile", { source: createSource(e.data.script) }); + emit("sourcesCompile", { source: createSource(e) }); } function onChangeFrame(frame, silent) { @@ -606,13 +449,21 @@ define(function(require, exports, module) { } /***** Methods *****/ + var process + + function getProxySource(_process) { + process = _process; + return false; + } function attach(s, reconnect, callback) { - if (v8ds) - v8ds.detach(); + if (v8dbg) + v8dbg.detach(); socket = s; + // socket.connect(); + socket.on("back", function(err) { sync(emit("getBreakpoints"), true, callback); }, plugin); @@ -620,20 +471,21 @@ define(function(require, exports, module) { emit("error", err); }, plugin); - v8ds = new V8DebuggerService(socket); - v8ds.attach(0, function(err) { + v8dbg = new DevtoolsProtocol(socket); + attached = false; + v8dbg.attach(process.runner.debugport, function(err, msg) { if (err) return callback(err); - v8dbg = new V8Debugger(0, v8ds); - // register event listeners - v8dbg.addEventListener("changeRunning", onChangeRunning); - v8dbg.addEventListener("break", onBreak); - v8dbg.addEventListener("exception", onException); - v8dbg.addEventListener("afterCompile", onAfterCompile); + v8dbg.on("changeRunning", onChangeRunning); + v8dbg.on("break", onBreak); + v8dbg.on("exception", onException); + v8dbg.on("afterCompile", onAfterCompile); onChangeFrame(null); + if (msg && msg.type == "connect") + reconnect = false; // This fixes reconnecting. I dont understand why, but without // this timeout during reconnect the getSources() call never // returns @@ -644,15 +496,11 @@ define(function(require, exports, module) { } function detach() { - if (!v8ds) - return; - - v8ds.detach(); - onChangeFrame(null); onChangeRunning(); if (v8dbg) { + v8dbg.detach(); // on detach remove all event listeners v8dbg.removeEventListener("changeRunning", onChangeRunning); v8dbg.removeEventListener("break", onBreak); @@ -660,10 +508,7 @@ define(function(require, exports, module) { v8dbg.removeEventListener("afterCompile", onAfterCompile); } - socket.unload(); - socket = null; - v8ds = null; v8dbg = null; attached = false; @@ -671,10 +516,10 @@ define(function(require, exports, module) { } function getSources(callback) { - v8dbg.scripts(4, null, false, function(scripts) { + v8dbg.scripts(function(scriptMap) { sources = []; - for (var i = 0, l = scripts.length; i < l; i++) { - var script = scripts[i]; + for (var i in scriptMap) { + var script = scriptMap[i]; if ((script.name || "").indexOf("chrome-extension://") === 0) continue; sources.push(createSource(script)); @@ -686,11 +531,11 @@ define(function(require, exports, module) { } function getSource(source, callback) { - v8dbg.scripts(4, [source.id], true, function(scripts) { - if (!scripts.length) + v8dbg.getScriptSource(source.id, function(script) { + if (!script || !script.scriptSource) return callback(new Error("File not found : " + source.path)); - var source = scripts[0].source + var source = script.scriptSource .replace(RE_NODE_PREFIX, "") .replace(RE_NODE_POSTFIX, ""); @@ -699,28 +544,10 @@ define(function(require, exports, module) { } function getFrames(callback, silent) { - v8dbg.backtrace(0, 1000, null, true, function(body, refs) { - function ref(id) { - for (var i = 0; i < refs.length; i++) { - if (refs[i].handle == id) { - return refs[i]; - } - } - return {}; - } - - var frames = []; - if (body && body.totalFrames > 0) { - body && body.frames.map(function(frame) { - var script = ref(frame.script.ref); - if (script.name && !/^native /.test(script.name)) - frames.push(createFrame(frame, script)); - }); - - var topFrame = frames[0]; - if (topFrame) - topFrame.istop = true; - } + v8dbg.backtrace(function(data) { + var frames = data.callFrames.map(function(frameData, index) { + return createFrame(data.callFrames, index); + }); emit("getFrames", { frames: frames }); callback(null, frames); @@ -728,11 +555,16 @@ define(function(require, exports, module) { } function getScope(frame, scope, callback) { - v8dbg.scope(scope.index, frame.index, true, function(body, refs, error) { + v8dbg.getProperties({ + objectId: scope.id, + ownProperties: false, + accessorPropertiesOnly: false, + generatePreview: true + }, function(body, error) { if (error) return callback(error); - var variables = body.object.properties.map(function(prop) { + var variables = (body.result || []).map(function(prop) { return createVariable(prop); }); @@ -743,47 +575,41 @@ define(function(require, exports, module) { } function getProperties(variable, callback) { - v8dbg.lookup([variable.ref], false, function(body, refs, err) { - if (err) return callback(err); + if (!variable.ref) + return callback(null, []); + v8dbg.getProperties({ + objectId: variable.ref, + ownProperties: true, + accessorPropertiesOnly: false, + generatePreview: true + }, function(body, error) { + if (error) + return callback(error); - var data = body[variable.ref]; - data && updateVariable(variable, data); - - var props = data.properties || []; - - if (props.length > 5000) { - props = [createVariable({ - name: "Too many properties", - value: { type: "error", value: "Found more than 5000 properties" }, - children: false - })]; - - variable.properties = props; - callback(null, props, variable); - return; - } - - lookup(props, false, function(err, properties) { - variable.properties = properties; - callback(err, properties, variable); + var properties = (body.result || []).map(function(prop) { + return createVariable(prop); }); + + variable.properties = properties; + + callback(null, properties); }); } function stepInto(callback) { - v8dbg.continueScript("in", null, callback); + v8dbg.stepInto(callback); } function stepOver(callback) { - v8dbg.continueScript("next", null, callback); + v8dbg.stepOver(callback); } function stepOut(callback) { - v8dbg.continueScript("out", null, callback); + v8dbg.stepOut(callback); } function resume(callback) { - v8dbg.continueScript(null, null, callback); + v8dbg.resume(callback); } function suspend(callback) { @@ -793,73 +619,31 @@ define(function(require, exports, module) { }); } - function lookup(props, includeSource, callback) { - // can happen for numbers. E.g when debugger stops on throw 1 - if (!props || !props.length) - return callback(null, []); - v8dbg.lookup(props.map(function(p) { return p.ref; }), - includeSource, function(body) { - if (!body) - return callback(new Error("No body received")); - - var properties = props.map(function(prop) { - prop.value = body[prop.ref]; - return createVariable(prop); - }); - - callback(null, properties); - }); - } - function setScriptSource(script, newSource, previewOnly, callback) { - newSource = NODE_PREFIX + newSource + NODE_POSTFIX; + newSource = NODE_PREFIX + newSource.replace(/^#!.*/, "") + NODE_POSTFIX; - v8dbg.changelive(script.id, newSource, previewOnly, function(e) { - var data = e; - - function cb() { - emit("setScriptSource", data); - callback(null, data); - } - - if (!e) - cb(new Error("Debugger could not update source of saved file.")); - else if (e.stepin_recommended) - stepInto(cb); - else if (e.result.stack_modified === false) { - getFrames(function(err, frames) { - if (!activeFrame || !frames.length) - return; // debugger isn't active - onChangeFrame(frames[0]); - emit("break", { - frame: activeFrame, - frames: frames - }); - }); - cb(); - } - else - cb(); + v8dbg.changelive(script.id, newSource, previewOnly, function(data, error) { + var errorMessage = "Debugger could not update source of saved file." + if (error) + return callback(new Error(errorMessage)); + if (data && data.exceptionDetails) + return callback(new Error(errorMessage + " " + data.exceptionDetails.text)); + + emit("setScriptSource", data); + callback(null, data); }); } function restartFrame(frame, callback) { - var frameIndex = frame && typeof frame == "object" ? frame.index : frame; - v8dbg.restartframe(frameIndex, function(body) { - if (body.result && body.result.stack_update_needs_step_in) { - stepInto(callback.bind(this, body)); - } - else { - callback.apply(this, arguments); - } + v8dbg.restartframe(frame.id, function(result, error) { + stepInto(callback); }); } function evaluate(expression, frame, global, disableBreak, callback) { - var frameIndex = frame && typeof frame == "object" ? frame.index : frame; - - v8dbg.evaluate(expression, frameIndex, global, - disableBreak, function(body, refs, error) { + v8dbg.evaluate(expression, frame, global, disableBreak, function(data) { + var value = data.result; + var error = data.error; var name = expression.trim(); if (error) { var err = new Error(error.message); @@ -870,18 +654,10 @@ define(function(require, exports, module) { var variable = createVariable({ name: name, - value: body + value: value, }); - if (variable.children) { - lookup(body.properties, false, function(err, properties) { - variable.properties = properties; - callback(null, variable); - }); - } - else { - callback(null, variable); - } + callback(null, variable); }); } @@ -890,50 +666,47 @@ define(function(require, exports, module) { var path = sm.source || bp.path; var line = sm.line || bp.line; var column = sm.column || bp.column; - var scriptId = getScriptIdFromPath(path); - if (!scriptId) { - // Wait until source is parsed - breakpointQueue.push([bp, callback]); - callback && callback(new Error("Source not available yet. Queuing request.")); + if (!path) { + // TODO find out why this happens + callback && callback(new Error("Ignoring breakpoint with invalid path.")); return false; } + + path = applyPathMap(path, "toExternal"); + + if (path[0] == "/") + path = stripPrefix + path; + else if (path[0] == "~") + path = c9.home + path.substr(1); + + path = c9.toExternalPath(path); - v8dbg.setbreakpoint("scriptId", scriptId, line, column, bp.enabled, - bp.condition, bp.ignoreCount, function(info) { - if (!info) - return callback && callback(new Error()); - - bp.id = info.breakpoint; - if (info.actual_locations) { - bp.actual = info.actual_locations[0]; - emit("breakpointUpdate", { breakpoint: bp }); - } - callback && callback(null, bp, info); - }); + v8dbg.setbreakpoint(path, line, column, bp.enabled, bp.condition, function(info) { + if (!info) + return callback && callback(new Error()); + + bp.id = info.breakpointId; + if (info.locations) { + var loc = info.locations[0]; + bp.actual = loc && { + line: loc.lineNumber, + column: loc.columnNumber, + scriptId: loc.scriptId, + }; + emit("breakpointUpdate", { breakpoint: bp }); + } + callback && callback(null, bp, info); + }); return true; } function changeBreakpoint(bp, callback) { - if (breakpointQueue.some(function(i) { - return i[0] === bp; - })) return; - - v8dbg.changebreakpoint(bp.id, bp.enabled, - bp.condition, bp.ignoreCount, function(info) { - callback && callback(null, bp, info); - }); + setBreakpoint(bp, callback); } function clearBreakpoint(bp, callback) { - if (breakpointQueue.some(function(i, index) { - if (i[0] === bp) { - breakpointQueue.splice(index, 1); - return true; - } - })) return; - v8dbg.clearbreakpoint(bp.id, callback); } @@ -950,133 +723,66 @@ define(function(require, exports, module) { }); } - function setVariable(variable, parents, value, frame, callback) { - // Get variable name - var names = [], scopeNumber, frameIndex = frame.index; - parents.reverse().forEach(function(p) { - // Assuming scopes are accessible - if (p.tagName == "variable") - names.push(p.name.replace(/"/g, '\\"')); - else if (p.tagName == "scope") - scopeNumber = p.index; - }); - names.push(variable.name); - - function handler(err, body) { - if (err) - return callback(err); - - variable.value = formatType(body); - variable.type = body.type; - variable.ref = body.handle; - variable.properties = body.properties || []; - variable.children = (body.properties || "").length ? true : false; - -// @todo - and make this consistent with getProperties -// if (body.constructorFunction) -// value.contructor = body.constructorFunction.ref; -// if (body.prototypeObject) -// value.prototype = body.prototypeObject.ref; - - if (variable.children) { - lookup(body.properties, false, function(err, properties) { - variable.properties = properties; - callback(null, variable); - }); - } - else { - callback(null, variable); - } - } - - // If it's a local variable set it directly - if (parents.length == (typeof scopeNumber == "number" ? 1 : 0)) - setLocalVariable(variable, value, scopeNumber || 0, frameIndex, handler); - // Otherwise set a variable or property - else - setAnyVariable(variable, parents[0], value, handler); - } - - function setLocalVariable(variable, value, scopeNumber, frameIndex, callback) { - v8dbg.simpleevaluate(value, null, true, [], function(body, refs, error) { - if (error) { - var err = new Error(error.message); - err.name = error.name; - err.stack = error.stack; - return callback(err); - } - - v8dbg.setvariablevalue(variable.name, body, scopeNumber, frameIndex, - function(body, refs, error) { - // lookup([variable.ref], false, function(err, properties) { - // variable.properties = properties; - // callback(null, variable); - // }); - - if (error) { - var err = new Error(error.message); - err.name = error.name; - err.stack = error.stack; - return callback(err); - } - - callback(null, body.newValue); - }); - }); - } - - function setAnyVariable(variable, parent, value, callback) { - var expression = "(function(a, b) { this[a] = b; })" - + ".call(__cloud9_debugger_self__, \"" - + variable.name + "\", " + value + ")"; - - v8dbg.simpleevaluate(expression, null, true, [{ - name: "__cloud9_debugger_self__", - handle: parent.ref - }], function(body, refs, error) { - if (error) { - var err = new Error(error.message); - err.name = error.name; - err.stack = error.stack; - return callback(err); - } - - callback(null, body); - }); + function setVariable(variable, value, frame, callback) { + v8dbg.setvariablevalue(variable, value, frame, callback); } function serializeVariable(variable, callback) { - var expr = "(function(fn){ return fn.toString() })" - + "(__cloud9_debugger_self__)"; - - v8dbg.simpleevaluate(expr, null, true, [{ - name: "__cloud9_debugger_self__", - handle: variable.ref - }], function(body, refs, error) { - callback(body.value); - }); + return callback(variable.value); } function setBreakBehavior(type, enabled, callback) { breakOnExceptions = enabled ? type == "all" : false; breakOnUncaughtExceptions = enabled ? type == "uncaught" : false; - v8dbg.setexceptionbreak(enabled ? type : "all", enabled, callback); + v8dbg.setexceptionbreak(enabled ? type : "none", callback); + } + + function setPathMap(v) { + if (!Array.isArray(v)) v = null; + pathMap = v && v.map(function(x) { + if (!x.toInternal || !x.toExternal) return; + var map = { + toInternal: {}, + toExternal: {} + }; + if (typeof x.toInternal.regex == "string") + map.toInternal.regex = new RegExp(x.toInternal.regex, "g"); + map.toInternal.replacement = x.toInternal.replacement; + if (typeof x.toExternal.regex == "string") + map.toExternal.regex = new RegExp(x.toExternal.regex, "g"); + map.toExternal.replacement = x.toExternal.replacement; + return map; + }).filter(Boolean); + } + + function applyPathMap(path, dir) { + if (!pathMap) + return path; + pathMap.forEach(function(record) { + var mapping = record[dir]; + path = path.replace(mapping.regex, mapping.replacement); + }); + return path; } /***** Lifecycle *****/ plugin.on("load", function() { - load(); - }); - plugin.on("enable", function() { - - }); - plugin.on("disable", function() { - + debug.registerDebugger(TYPE, plugin); }); plugin.on("unload", function() { - unload(); + debug.unregisterDebugger(TYPE, plugin); + + breakOnExceptions = null; + breakOnUncaughtExceptions = null; + attached = false; + v8dbg = null; + state = null; + activeFrame = null; + sources = null; + socket = null; + pathMap = null; }); /***** Register and define API *****/ @@ -1095,6 +801,26 @@ define(function(require, exports, module) { * @class debugger.implementation */ plugin.freezePublicAPI({ + /** + * Specifies the features that this debugger implementation supports + * @property {Object} features + * @property {Boolean} features.scripts Able to download code (disable the scripts button) + * @property {Boolean} features.conditionalBreakpoints Able to have conditional breakpoints (disable menu item) + * @property {Boolean} features.liveUpdate Able to update code live (don't do anything when saving) + * @property {Boolean} features.updateWatchedVariables Able to edit variables in watches (don't show editor) + * @property {Boolean} features.updateScopeVariables Able to edit variables in variables panel (don't show editor) + * @property {Boolean} features.setBreakBehavior Able to configure break behavior (disable break behavior button) + * @property {Boolean} features.executeCode Able to execute code (disable REPL) + */ + features: { + scripts: true, + conditionalBreakpoints: true, + liveUpdate: true, + updateWatchedVariables: true, + updateScopeVariables: true, + setBreakBehavior: true, + executeCode: true + }, /** * The type of the debugger implementation. This is the identifier * with which the runner selects the debugger implementation. @@ -1133,6 +859,49 @@ define(function(require, exports, module) { get breakOnUncaughtExceptions() { return breakOnUncaughtExceptions; }, _events: [ + /** + * Fires when the debugger is attached. + * @event attach + * @param {Object} e + * @param {debugger.Breakpoint[]} e.breakpoints A list of breakpoints that is set in the running process + */ + "attach", + /** + * Fires when the debugger is detached. + * @event detach + */ + "detach", + /** + * Fires when execution is suspended (paused) + * @event suspend + */ + "suspend", + /** + * Fires when the source of a file is updated + * @event setScriptSource + * @param {Object} e + */ + "setScriptSource", + /** + * Fires when the socket experiences an error + * @event error + */ + "error", + /** + * Fires when the current list of breakpoints is needed + * @event getBreakpoints + */ + "getBreakpoints", + /** + * Fires when a breakpoint is updated. This can happen when it + * is set at a location which is not an expression. Certain + * debuggers (such as v8) will move the breakpoint location to + * the first expression that's next in source order. + * @event breakpointUpdate + * @param {Object} e + * @param {debugger.Breakpoint} e.breakpoint + */ + "breakpointUpdate", /** * Fires when the debugger hits a breakpoint. * @event break @@ -1350,12 +1119,19 @@ define(function(require, exports, module) { setVariable: setVariable, /** - * + * Starts a frame (usually a function) from the first expression in that frame. + * @param {debugger.Frame} frame The frame to restart. + * @param {Function} callback + * @param {Function} callback Called when the frame is restarted. */ restartFrame: restartFrame, /** - * + * Retrieve the value of a variable + * @param {debugger.Variable} variable The variable for which to retrieve the value + * @param {Function} callback + * @param {Function} callback Called when the value is retrieved + * @param {String} callback.value The value of the variable */ serializeVariable: serializeVariable, @@ -1366,7 +1142,18 @@ define(function(require, exports, module) { * @param {Function} callback Called after the setting is changed. * @param {Error} callback.err The error if any error occured. */ - setBreakBehavior: setBreakBehavior + setBreakBehavior: setBreakBehavior, + + /** + * Returns the source of the proxy + */ + getProxySource: getProxySource, + + /** + * @ignore + * Experimental method for meteor runner + */ + setPathMap: setPathMap }); register(null, { diff --git a/plugins/c9.ide.run.debug/debuggers/socket.js b/plugins/c9.ide.run.debug/debuggers/socket.js index a9cc73ea..70d186cc 100644 --- a/plugins/c9.ide.run.debug/debuggers/socket.js +++ b/plugins/c9.ide.run.debug/debuggers/socket.js @@ -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 }; diff --git a/plugins/c9.ide.run/runners-docker/Node.js (default).run b/plugins/c9.ide.run/runners-docker/Node.js (default).run index 8e6be838..e6edcb7b 100644 --- a/plugins/c9.ide.run/runners-docker/Node.js (default).run +++ b/plugins/c9.ide.run/runners-docker/Node.js (default).run @@ -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", diff --git a/plugins/c9.ide.run/runners/Node.js.run b/plugins/c9.ide.run/runners/Node.js.run index 0a96b4da..1879613f 100644 --- a/plugins/c9.ide.run/runners/Node.js.run +++ b/plugins/c9.ide.run/runners/Node.js.run @@ -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,