/** * This file is part of IKPdb Cloud9 debugger plugin. * Copyright (c) 2016 by Cyril MORISSE, Audaxis * Licence MIT. See LICENCE at repository root */ define(function(require, exports, module) { main.consumes = [ "Plugin", "c9", "util", "debugger", "dialog.error", "notification.bubble" ]; main.provides = ["ikpdb"]; return main; function main(options, imports, register) { /****( IDE connection )*****/ var Plugin = imports.Plugin; var util = imports.util; var debug = imports["debugger"]; var showError = imports["dialog.error"].show; var bubble = imports["notification.bubble"]; var panels = imports.panels; var settings = imports.settings; var Frame = debug.Frame; var Source = debug.Source; var Breakpoint = debug.Breakpoint; var Variable = debug.Variable; var Scope = debug.Scope; /***** Other dependencies *****/ var IKPdbService = require("./lib/IKPdbService"); /***** Initialization *****/ var TYPE = "pythondebug"; var PROXY = require("text!./netproxy.js"); var plugin = new Plugin("Cyril Morisse,Audaxis", main.consumes); var emit = plugin.getEmitter(); // TODO: emit.setMaxListeners(1000); /** * State variables */ var attached = false, state = null, stack = null, ikpdbs = null; var debugger_socket = null; var SCOPES = ["Parameters", "Locals"]; /***** Lifecycle *****/ plugin.on("load", function() { debug.registerDebugger(TYPE, plugin); }); plugin.on("unload", function() { debug.unregisterDebugger(TYPE, plugin); attached = false; stack = null; ikpdbs = null; state = null; debugger_socket = null; }); /*****(Event handlers and Methods )*****/ function sendCommand(command, args, callback) { ikpdbs.sendCommand(command, args, function(err, reply) { /***** * Process the messages IKPdb could send * Note that error messages ar only displayed if IKPdb returned * an error. */ if (reply) { // Display info and warning messages reply.info_messages.forEach(function (iMessage) { console.log(iMessage); }); reply.warning_messages.forEach(function (wMessage) { bubble.popup(wMessage, false); }); } if (err) { // Display errors var errMessage; if (reply) { errMessage = reply.error_messages.join(" "); showError(errMessage, 10000); } return callback && callback(err); } if (reply.result.executionStatus) setState(reply.result.executionStatus); callback && callback(err, reply); }); } /* Called by Cloud9 when user start execution */ function runScript(callback) { sendCommand("runScript", {}, callback); } /* Called by Cloud9 when user clic on the Step over / F10 button */ function stepOver(callback) { sendCommand("stepOver", {}, callback); } /* Called by Cloud9 when user clic on the Step over / F10 button */ function stepInto(callback) { sendCommand("stepInto", {}, callback); } /* Called by Cloud9 when user clic on the Step over / F10 button */ function stepOut(callback) { sendCommand("stepOut", {}, callback); } /* Called by Cloud9 when user clic on the Resume / F8 button */ function resume(callback) { sendCommand("resume", {}, callback); } /* Called by Cloud9 when user clic on Suspend */ function suspend(callback) { sendCommand("suspend", {}, callback); } /** * Build individual variable objects from IKPdb output */ function buildVariable(variable, scope) { if (variable == null) return; return new Variable({ // ref: (variable.objname) ? variable.objname : variable.name, ref: variable.id, // name: (variable.exp) ? variable.exp : variable.name, name: variable.name, value: variable.value, type: variable.type, //children: (variable.numchild && variable.numchild > 0), children: variable.children_count, properties: null, scope: scope }); } /** * Create a scope and variables from data received from IKPdb */ function buildScopeVariables(frame_vars, scope_index, frame_index, vars) { var scope = new Scope({ index: scope_index, type: SCOPES[scope_index], frameIndex: frame_index }); for (var i = 0, j = vars.length; i < j; i++) { frame_vars.push(buildVariable(vars[i], scope)); } } function buildFrame(thread_id, frame, i) { var variables = []; // build scopes and variables for this frame /* * Right now Cloud9 does not allow to diplay parameter appart from * local vars * TODO: Discuss with Cloud9 how to render parameters appart from locals in Debugger panels */ //buildScopeVariables(variables, 0, i, frame.f_locals); if (typeof frame.f_locals !== "undefined") { buildScopeVariables(variables, 1, i, frame.f_locals); } // parse file from path return new Frame({ index: i, name: frame.name, column: 0, id: frame.id, line: parseInt(frame.line_number, 10) - 1, // IKPdb lines are 1 based path: util.normalizePath("/" + frame.file_path), sourceId: frame.file_path, thread: thread_id, istop: (i === 0), variables: variables }); } /** * Process asynchonous messags received from remote debugger * like breakpoint hit or program termination */ function programBreak(message) { if (message.command === 'programEnd') { return detach(); // detach() update state } // no error, only frames stack = []; // process frames var frames = message.frames; for (var i = 0, j = frames.length; i < j; i++) { stack.push(buildFrame(frames[i].thread, frames[i], i)); } setState("stopped"); emit("frameActivate", { frame: stack[0] }); /***** * Process the 3 levels of messags IKPdb can send. */ message.info_messages.forEach(function (iMessage) { console.log(iMessage); }); message.warning_messages.forEach(function (wMessage) { bubble.popup(wMessage, false); }); message.error_messages.forEach(function (eMessage) { showError(eMessage, 10000); }); /** * Exceptions handling * We build a variable describing the exception * that c9 will inject into "Watch Expressions". */ if (message.exception) { showError("IKpdb has detected an unmanaged exception \"" + message.exception.type + "\":" + message.exception.info + ". Execution has stopped!", 10000); var exception = new Variable({ ref: null, name: message.exception.type, value: message.exception.info, type: "Exception", children: false, properties: null, scope: "Locals" }); emit("exception", { frame: stack[0], frames: stack, exception: exception }); debug.getElement("btnSuspend").setAttribute("disabled", true); debug.getElement("btnStepOut").setAttribute("disabled", true); debug.getElement("btnStepInto").setAttribute("disabled", true); debug.getElement("btnStepOver").setAttribute("disabled", true); } else { emit("break", { frame: stack[0], frames: stack }); if (stack.length == 1) debug.getElement("btnStepOut").setAttribute("disabled", true); } } /***** * Set the debugger state and emit state change */ function setState(_state) { if (state === _state) return; state = _state; emit("stateChange", { state: state }); } /***** * we can't use this since breakpoints function are called by cloud9 * not by user * TODO: check with cloud9 a way to intercept user actions */ function breakpointManagementWarning(commandSpecificMessage) { return; if (state == "running") { var msg = "IKPdpb Error: You can't manage breakpoints while" + " program is running. Breakpoints can be manipulated " + "before debugger launch or when program is stopped. " + commandSpecificMessage; showError(msg, 10000); bubble.popup(commandSpecificMessage, false); } } function setBreakpoint(bp, callback) { // TODO: Check with c9 how we can prevent user to set breakpoint breakpointManagementWarning("Breakpoint creation request will be send to debugger at next programm break !."); bp.data.file_name = bp.data.path.substring(1); // IKPdb expects a filename so we remove leading / bp.data.line_number = bp.data.line + 1; // IKPdb expects 1 based lines sendCommand("setBreakpoint", bp.data, function(err, reply) { if (err) return callback && callback(err); if (reply.commandExecStatus == "error") return callback && callback(new Error("Can't set breakpoint")); bp.id = reply.result.breakpoint_number; callback && callback(null, bp, {}); }); } /** * Called by c9 when user clic on enable / disable breakpoints */ function changeBreakpoint(bp, callback) { // TODO: Check with c9 how we can prevent user to set breakpoint breakpointManagementWarning("Breakpoint modification request will be send to debugger at next programm break !."); if (!bp.data) { console.error("Missing bp.data in changeBreakpoint(", bp, ") "); } bp.data.breakpoint_number = bp.id; sendCommand("changeBreakpointState", bp.data, function(err) { callback && callback(err, bp); }); } /** * Called by c9 when user remove an existing breakpoint. */ function clearBreakpoint(bp, callback) { // TODO: Check with c9 how we can prevent user to set breakpoint breakpointManagementWarning("Breakpoint removal request will be send to debugger at next programm break !."); bp.data.breakpoint_number = bp.id; sendCommand("clearBreakpoint", bp.data, function(err) { callback && callback(err, bp); }); } /** * send breakpoints to debugger and attach when done */ function manyBreakpoints(breakpoints, command, callback) { function _setBPs(breakpoints, failed, callback, i) { // run callback once we've exhausted setting breakpoints if (i == breakpoints.length) { callback(breakpoints, failed); return; } command(breakpoints[i], function(err, bp) { if (err) { // breakpoint failure, remove it before going on failed.push(breakpoints.splice(i, 1)); _setBPs(breakpoints, failed, callback, i); } else { breakpoints[i].id = bp.id; _setBPs(breakpoints, failed, callback, i + 1); } }); } _setBPs(breakpoints, [], callback, 0); } function sync(begin, callback) { // send breakpoints to ikpdb and attach when done var localBkpts = emit("getBreakpoints"); listBreakpoints(function(err, remoteBkpts) { if (err) return callback(err); /* There exist two sets of breakpoints. One local as shown * in the GUI, L, and one "remote" that already exists in * IKPdb's state, R. * Syncing L and R must prioritize L's elements. We'll * create three sets: * to_remove = R\L (or {x∈R|x∉L}) * BPs present in R but not in L, must be removed from R * to_add = L/R (or {x∈L|x∉R}) * BPs present in L but not in R, must be added to R * synced = L∩R * BPs already in both. */ var to_add = []; var synced = []; // compare the GUI breakpoints to those already created for (var i = 0, j = localBkpts.length; i < j; i++) { var bp = localBkpts[i]; var missing = true; // test for membership of bp in remoteBkpts for (var x = 0, y = remoteBkpts.length; x < y; x++) { var rbp = remoteBkpts[x]; if (bp.path == rbp.path && bp.line == rbp.line && bp.condition == rbp.condition) { // make sure synced BP has correct id bp.id = rbp.id; // track necessary removals by removing used BPs remoteBkpts.splice(x, 1); missing = false; break; } } if (missing) to_add.push(bp); else synced.push(bp); } // notify IKPDb of new breakpoints manyBreakpoints(to_add, setBreakpoint, function(added, fail) { // successfully created BPs are now synced synced = synced.concat(added); // now remove extraneous BPs manyBreakpoints(remoteBkpts, clearBreakpoint, function(cleared, clrfail) { // BPs that failed to remove need to be present locally synced = synced.concat(clrfail); attached = true; emit("attach", { breakpoints: synced }); if (begin) runScript(callback); else sendCommand("getStatus", {}, callback); }); }); }); } /* * Called by Cloud9 to retreive value of "Watch Expressions" and * results of expressions types in "Immediate (Debuger)"" panel * WARNING: Right now only expressions are supported ; user * can't type a statement in immediate panel */ function evaluate(expression, frame, global, disableBreak, callback) { var args = { "expression": expression, "frame": (frame && frame.id) || null, "thread": (frame && frame.thread) || null, "global": global, "disableBreak": disableBreak }; sendCommand("evaluate", args, function(err, reply) { if (err) { return callback(new Error("No value")); } /* TODO: remove if unneeded else if (reply.commandExecutionStatus == "error") return callback(new Error(reply.messages.msg)); */ callback(null, new Variable({ name: expression, value: reply.result.value, type: reply.result.type, children: false })); }); } function setVariable(variable, value, frame, callback) { var args = { "frame": frame.id, "name": variable.name, "value": value }; sendCommand("setVariable", args, function(err, reply) { if (err) return callback && callback(err); callback && callback(null, variable); }); } /** * properly handle reconnect */ function reconnectSync(callback) { if (!callback) callback = function() {}; sendCommand("reconnect", {}, function(err, reply) { var restart = !err && reply.executionStatus == "running"; sync(restart, callback); }); } function attach(_socket, reconnect, callback) { //console.debug("Entering IKPdb Plugin attach(_socket", _socket, "reconnect=", reconnect," ,callback=", callback,")"); debugger_socket = _socket; // The back event is fired when the socket reconnects debugger_socket.on("back", function() { reconnectSync(); }, plugin); // The error event is fired when the socket fails to connect debugger_socket.on("error", function(err) { console.error("IKPb debugger socket error !", err); emit("error", err); }, plugin); ikpdbs = new IKPdbService(debugger_socket, programBreak); ikpdbs.attach(function() { emit("connect"); // if we're reconnecting, check IKPdb's state if (reconnect) reconnectSync(callback); else sync(true, callback); }); // Update GUI emit("frameActivate", { frame: null }); } /* * Clean up the debugger connection */ function detach() { // console.log("entering detach()"); if (ikpdbs) ikpdbs.detach(); emit("frameActivate", { frame: null }); setState(null); debugger_socket = null; attached = false; ikpdbs = null; emit("detach"); } function getProxySource(process) { return PROXY.replace(/\/\/.*/g, "") .replace(/[\n\r]/g, "") .replace(/\{DEBUGGED_PROCESS_PORT\}/, process.runner.debugport) .replace(/\{DEBUGGED_PROCESS_HOST\}/, process.runner.debughost || "127.0.0.1"); } /* Not applicable */ function getSources(callback) { var sources = [new Source()]; callback(null, sources); emit("sources", { sources: sources }); } /* Not applicable */ function getSource(source, callback) { callback(null, new Source()); } function getFrames(callback, silent) { var frames = []; emit("getFrames", { frames: frames }); callback(null, frames); } function getScope(frame, scope, callback) { callback(null, scope.variables, scope, frame); } /* called by cloud9 when user request to expand variable's children */ function getProperties(variable, callback) { // request children of a variable var args = { id: variable.ref, name: variable.name }; sendCommand("getProperties", args, function(err, reply) { if (err) return callback && callback(err); else if (reply.result.properties.length === 0) return callback && callback(new Error("No children")); var children = []; reply.result.properties.forEach(function (child) { children.push(buildVariable(child, variable.scope)); }); variable.properties = children; callback && callback(null, children, variable); }); } /*** * Retrieves a list of all the breakpoints that are set in the debugger. */ function listBreakpoints(callback) { sendCommand("getBreakpoints", {}, function(err, reply) { if (err) { console.error("getBreakpoints() error with response=", reply); return callback && callback(err, reply); } // we reprocess breakpoints list to adjust line numbers and file paths reply.result.forEach(function(bp) { bp.line = bp.line_number - 1; bp.id = bp.breakpoint_number; bp.path = util.normalizePath(bp.file_name); }); var breakpointList = reply.result; callback(null, breakpointList); }); } /***** Register and define API *****/ /** * TODO: comment * @singleton */ 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: false, conditionalBreakpoints: true, liveUpdate: false, updateWatchedVariables: false, updateScopeVariables: true, setBreakBehavior: false, executeCode: true }, /** * The type of the debugger implementation. This is the identifier * with which the runner selects the debugger implementation. * @property {String} type * @readonly */ type: TYPE, /** * Besides features and type, there are only four properties for * which we should define property getters. * These are state, attached, breakOnExceptions * and breakOnUncaughtExceptions. /** * @property {null|"running"|"stopped"} state The state of the debugger process * * * * * *
Value Description
null process doesn't exist
"stopped" paused on breakpoint
"running" process is running
* @readonly */ get state() { return state; }, /** * */ get attached() { return attached; }, /** * Whether the debugger will break when it encounters any exception. * This includes exceptions in try/catch blocks. * @property {Boolean} breakOnExceptions * @readonly */ get breakOnExceptions() { return false; }, /** * Whether the debugger will break when it encounters an uncaught * exception. * @property {Boolean} breakOnUncaughtExceptions * @readonly */ get breakOnUncaughtExceptions() { return true; }, _events: [ "attach", "detach", /** * Fires when the current list of breakpoints is needed * @event getBreakpoints */ "getBreakpoints", ], /** * Attaches the debugger to the started process. * @param {Object} runner A runner as specified by {@link run#run}. * @param {debugger.Breakpoint[]} breakpoints The set of breakpoints that should be set from the start */ attach: attach, detach: detach, resume: resume, suspend: suspend, stepOver: stepOver, /** * Retrieves and sets the properties of a variable. * @param {debugger.Variable} variable The variable for which to retrieve the properties. * @param {Function} callback Called when the properties are loaded * @param {Error} callback.err The error object if an error occured. * @param {debugger.Variable[]} callback.properties A list of properties of the variable. * @param {debugger.Variable} callback.variable The variable to which the properties belong. */ getProperties: getProperties, stepInto: stepInto, stepOut: stepOut, getProxySource: getProxySource, /** * Adds a breakpoint to a line in a source file. * @param {debugger.Breakpoint} breakpoint The breakpoint to add. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint} callback.breakpoint The added breakpoint * @param {Object} callback.data Additional debugger specific information. */ setBreakpoint: setBreakpoint, clearBreakpoint: clearBreakpoint, /** * Evaluates an expression in a frame or in global space. * @param {String} expression The expression. * @param {debugger.Frame} frame The stack frame which serves as the contenxt of the expression. * @param {Boolean} global Specifies whether to execute the expression in global space. * @param {Boolean} disableBreak Specifies whether to disabled breaking when executing this expression. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Variable} callback.variable The result of the expression. */ evaluate: evaluate, /** * Defines how the debugger deals with exceptions. * @param {"all"/"uncaught"} type Specifies which errors to break on. * @param {Boolean} enabled Specifies whether to enable breaking on exceptions. * @param {Function} callback Called after the setting is changed. * @param {Error} callback.err The error if any error occured. */ setBreakBehavior: function() {}, /** * Updates properties of a breakpoint * @param {debugger.Breakpoint} breakpoint The breakpoint to update. * @param {Function} callback Called after the expression has executed. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint} callback.breakpoint The updated breakpoint */ changeBreakpoint: changeBreakpoint, /* * Retrieves a list of all the breakpoints that are set in the * debugger. * @param {Function} callback Called when the breakpoints are retrieved. * @param {Error} callback.err The error if any error occured. * @param {debugger.Breakpoint[]} callback.breakpoints A list of breakpoints */ listBreakpoints: listBreakpoints, /** * Sets the value of a variable. * @param {debugger.Variable} variable The variable to set the value of. * @param {Mixed} value The new value of the variable. * @param {debugger.Frame} frame The frame to which the variable belongs. * @param {Function} callback * @param {Function} callback Called when the breakpoints are retrieved. * @param {Error} callback.err The error if any error occured. * @param {Object} callback.data Additional debugger specific information. */ setVariable: setVariable, }); register(null, { "ikpdb": plugin }); } });