diff --git a/CODING_STYLE.md b/CODING_STYLE.md deleted file mode 100644 index 4b7bf2a7c8..0000000000 --- a/CODING_STYLE.md +++ /dev/null @@ -1,131 +0,0 @@ -# cocos2d-x C++ coding sytle - - -## Detailed information - -Please, refer to this document for a detailed version of the cocos2d-x C++ coding sytle: - -* [cocos2d-x c++ coding style](http://www.cocos2d-x.org/wiki/Cocos2d_c++_coding_style) - - -## Quick Sample - -Use this sample as a quick reference. But DO READ the detailed doc for more info. - -Header file: - -```c++ -/** - * We use Doxygen strings for documentation. - * All public classes, methods, structs should be documented using Doxygen Strings - */ -class CC_DLL Sprite : public NodeRGBA, public TextureProtocol -{ /* class braces start in a new line */ - -/* no indentation here for public, protected or private */ -/* First add all the "public" stuff, then all the "protected" stuff, and finally all the "private" stuff -public: - - /* we don't use tabs, we use spaces, and we use a 4 space identation */ - /* 1st add all static const */ - static const int INDEX_NOT_INITIALIZED = -1; - - /* then add all the creators and other relevant static methods */ - static Sprite* create(); - static Sprite* create(const char *filename); - static Sprite* create(const char *filename, const Rect& rect); - - /* if applicable, then add the consturctors / destructors */ - Sprite(); - virtual ~Sprite(void); - - /* then add all the initialization methods */ - /* notice that they are in the same order as the creators */ - virtual bool init(void); - virtual bool initWithTexture(Texture2D *texture); - virtual bool initWithTexture(Texture2D *texture, const Rect& rect); - - - - /* then add the regular instace methods */ - virtual void updateTransform(void); - virtual SpriteBatchNode* getBatchNode(void); - virtual void setBatchNode(SpriteBatchNode *spriteBatchNode); - - - /* then add all the overriden methods */ - /* notice that all overriden methods must use the 'override' keyword */ - /* overriden methods are not forced to have doxygen strings UNLESS they change the behavior in a non obvios way */ - virtual void setPosition(const Point& pos) override; - virtual void setRotation(float rotation) override; - virtual void setRotationX(float rotationX) override; - - - /* once you finish with the 'public' methods, start with the 'protected' ones */ -protected: - - /* protected methods are not forced to have Doxygen strings, but if they have it, better */ - void updateColor(void); - virtual void setTextureCoords(Rect rect); - - /* After adding all the methods, add the ivars */ - /* all ivars must start with _ */ - /* Do not use Hungarian notation */ - TextureAtlas* _textureAtlas; - int _atlasIndex; - SpriteBatchNode* _batchNode; -}; - -``` - -Implementation file: - -```c++ -/* Do not use doxygen comments on the implementation file */ - -/* The methos MUST be in the same order as where declared in the header file */ - -Sprite* Sprite::create(const char *filename) -{ - /* Don't use tabs. Use spaces. Use 4-space indentation */ - Sprite *sprite = new Sprite(); - - /* put curly braces in the same line as in the 'if'*/ - /* leave a space between the 'if' and the '(' */ - /* don't leave spaces between '()' */ - if (sprite && sprite->initWithFile(filename)) { - sprite->autorelease(); - return sprite; - } - CC_SAFE_DELETE(sprite); - return NULL; -} - -/* Initialization list can be indented to 0 spaces, or to 4 spaces. If in doubt, be consistent with the indentation already used in the file */ -/* Only use the Initialization lists for types that can't fail when initialized */ -Sprite::Sprite() -: _shouldBeHidden(false) -, _texture(nullptr) -, _physicsBody(nullptr) -{ -} - -/* use the 'initXXX' methods to initialize types that might fail */ -bool Sprite::initWithTexture(Texture2D *texture, const Rect& rect, bool rotated) -{ - /* it ok not use use curly braces */ - if (something) - do_something(); - else - something_else(); - - /* but if you use curly branches in one branch, all the branches should use curly branches */ - if (something) { - do_something1(); - do_something2(); - } else { - so_something_else(); - } -} - -``` diff --git a/Emscripten.TODO b/Emscripten.TODO deleted file mode 100644 index a00ca3ce9e..0000000000 --- a/Emscripten.TODO +++ /dev/null @@ -1,60 +0,0 @@ -* Need to figure out how to get correct screen resolutions consistently. - -* Touch handler screwed up after TouchesTest? MenuTest? (TestCpp sample) - -* TTF Font rendering is slow and seems to wrap around by a few pixels - horizontally. Need to investigate here, but I suspect the right answer is to - offload font rendering to an offscreen canvas and let the browser handle it. - Potentially creates new challenges in packaging which would need to be solved - somehow. - - - -### -DONE: -### - -* Need to switch to server-side buffers in -- cocos2dx/draw_nodes/CCDrawingPrimitives - -* Need to compile with -O2 -- possible -s VERBOSE=1 will give a clue? - -* SchedulerTest crashes - - cocos2d::CCNode::boundingBox() - -* Parallax Test crashes - - cocos2d::CCAtlasNode::calculateMaxItems() - -* Particle Test crashes - - TIFFClientOpen not a function. Looks like we need to build and link in libtiff. - -* cocos2d::CCAtlasNode::calculateMaxItems() throwing an error; related to - CCTextureAtlas? Preventing particle test from working. - -* Need to switch to server-side buffers in -- cocos2dx/draw_nodes/CCDrawNode.cpp -- Think this is fixed by undef CC_TEXTURE_ATLAS_USE_VAO? -- cocos2dx/particle_nodes/CCParticleSystemQuad.cpp -- Think this is fixed by CC_REBIND_INDICES_BUFFER? -- cocos2dx/textures/CCTextureAtlas.cpp -- Think this works already. - -* Layer Test crashes - - cocos2d::CCLabelBMFont::create(char const*, char const*, float, cocos2d::CCTextAlignment, cocos2d::CCPoint) - -* IntervalTest crashes - - cocos2d::CCLabelBMFont::create(char const*, char const*, float, cocos2d::CCTextAlignment, cocos2d::CCPoint) - -* TileMap Test crashes - - void CCNode::insertChild(CCNode* child, int z) - -* LabelTest crashes - - cocos2d::CCAtlasNode::calculateMaxItems() - -* ZwoptexTest crashes. - - ZwoptexGenericTest::onEnter() - -* ChipmunkTest crashes: - Aborting due to Chipmunk error: Moment of Inertia must be positive and non-zero. - Failed condition: moment > 0.0f - Source:../src/cpBody.c:151 - -* Add linkage to libjpeg to get RenderTexture test to work. - diff --git a/RELEASE_NOTES b/RELEASE_NOTES deleted file mode 100644 index f10872da6c..0000000000 --- a/RELEASE_NOTES +++ /dev/null @@ -1,4 +0,0 @@ -===== cocos2d-x 3.0 Release Notes ===== - -Please, read the online release notes document: -http://www.cocos2d-x.org/wiki/Release_Notes_for_Cocos2d-x_v300 \ No newline at end of file diff --git a/cocos/scripting/javascript/bindings/js/debugger/DevToolsUtils.js b/cocos/scripting/javascript/bindings/js/debugger/DevToolsUtils.js new file mode 100644 index 0000000000..26d0597644 --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/DevToolsUtils.js @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function utf16to8(str) { + var out, i, len, c; + + out = ""; + len = str.length; + for(i = 0; i < len; i++) + { + c = str.charCodeAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) + { + out += str.charAt(i); + } + else if (c > 0x07FF) + { + out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); + out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); + } + else + { + out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); + } + } + return out; +} + +function utf8to16(str) { + var out, i, len, c; + var char2, char3; + + out = ""; + len = str.length; + i = 0; + while(i < len) { c = str.charCodeAt(i++); switch(c >> 4) + { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += str.charAt(i-1); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = str.charCodeAt(i++); + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = str.charCodeAt(i++); + char3 = str.charCodeAt(i++); + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + } + } + + return out; + } + +var dump = function(msg) { + log(msg); +}; + +/* General utilities used throughout devtools. */ + +/* Turn the error e into a string, without fail. */ +this.safeErrorString = function safeErrorString(aError) { + try { + var s = aError.toString(); + if (typeof s === "string") + return s; + } catch (ee) { } + + return ""; +} + +/** + * Report that |aWho| threw an exception, |aException|. + */ +this.reportException = function reportException(aWho, aException) { + let msg = aWho + " threw an exception: " + safeErrorString(aException); + if (aException.stack) { + msg += "\nCall stack:\n" + aException.stack; + } + + dump(msg + "\n"); + + // if (Components.utils.reportError) { + // /* + // * Note that the xpcshell test harness registers an observer for + // * console messages, so when we're running tests, this will cause + // * the test to quit. + // */ + // Components.utils.reportError(msg); + // } +} + +/** + * Given a handler function that may throw, return an infallible handler + * function that calls the fallible handler, and logs any exceptions it + * throws. + * + * @param aHandler function + * A handler function, which may throw. + * @param aName string + * A name for aHandler, for use in error messages. If omitted, we use + * aHandler.name. + * + * (SpiderMonkey does generate good names for anonymous functions, but we + * don't have a way to get at them from JavaScript at the moment.) + */ +this.makeInfallible = function makeInfallible(aHandler, aName) { + if (!aName) + aName = aHandler.name; + + return function (/* arguments */) { + try { + return aHandler.apply(this, arguments); + } catch (ex) { + let who = "Handler function"; + if (aName) { + who += " " + aName; + } + reportException(who, ex); + } + } +} diff --git a/cocos/scripting/javascript/bindings/js/debugger/README.md b/cocos/scripting/javascript/bindings/js/debugger/README.md new file mode 100644 index 0000000000..9e1d7bebd0 --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/README.md @@ -0,0 +1,54 @@ +Remote Debugging By Using FireFox +================================= + +Requirement +----------- + +* Firefox: From v24 + +How To Use +---------- + +### Prepare ### + +Please refer to https://developer.mozilla.org/en-US/docs/Tools/Remote_Debugging . + +### Enable Debugger Support For Your JSB Project ### + +``` +bool AppDelegate::applicationDidFinishLaunching() +{ + ... + + ScriptingCore* sc = ScriptingCore::getInstance(); + sc->addRegisterCallback(register_all_cocos2dx); + sc->addRegisterCallback(register_all_cocos2dx_extension); + sc->addRegisterCallback(register_cocos2dx_js_extensions); + sc->addRegisterCallback(jsb_register_chipmunk); + sc->addRegisterCallback(register_all_cocos2dx_extension_manual); + sc->addRegisterCallback(register_CCBuilderReader); + sc->addRegisterCallback(jsb_register_system); + sc->addRegisterCallback(JSB_register_opengl); + + sc->start(); + +#if defined(COCOS2D_DEBUG) && (COCOS2D_DEBUG > 0) + sc->enableDebugger(); // Enable debugger here +#endif + + ... +} +``` + +Run your game. + +### Open Firefox And Follow The Step As Follows ### + + +![pic 1](https://lh5.googleusercontent.com/-HoxLGBdV2J0/UlZ7ZoFUjyI/AAAAAAAAADM/68GDaCQ1vP0/s0-I/Firefox-Remote-Debug01.jpg) +![pic 2](https://lh6.googleusercontent.com/-7FDIHAYsKAY/UlZ7Yf8W-pI/AAAAAAAAAFQ/joG0AymnuBk/s0-I/Firefox-Remote-Debug02.jpg) +![pic 3](https://lh4.googleusercontent.com/-idvnMRGcGy8/UlZ7Wj6DDuI/AAAAAAAAAC0/L9IVyHLNqeQ/s0-I/Firefox-Remote-Debug04.jpg) +![pic 4](https://lh6.googleusercontent.com/-YuZj7JGAtFE/UlZ9DDGDczI/AAAAAAAAAEQ/D2qIedjP5FU/s0-I/Firefox-Remote-Debug04.png.png) +![pic 5](https://lh3.googleusercontent.com/-cdIcNa3jT5c/UlZ9uapf3OI/AAAAAAAAAEg/MGq3vLHsauw/s0-I/Firefox-Remote-Debug05.png) +![pic 6](https://lh5.googleusercontent.com/-T79-o5ylJKI/UlZ_JJQe3MI/AAAAAAAAAE8/F63fSVxlJKs/s0-I/Firefox-Remote-Debug06.png) + diff --git a/cocos/scripting/javascript/bindings/js/debugger/actors/root.js b/cocos/scripting/javascript/bindings/js/debugger/actors/root.js new file mode 100644 index 0000000000..aa0d85342b --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/actors/root.js @@ -0,0 +1,329 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* Root actor for the remote debugging protocol. */ + +/** + * Methods shared between RootActor and BrowserTabActor. + */ + +/** + * Populate |this._extraActors| as specified by |aFactories|, reusing whatever + * actors are already there. Add all actors in the final extra actors table to + * |aPool|. + * + * The root actor and the tab actor use this to instantiate actors that other + * parts of the browser have specified with DebuggerServer.addTabActor antd + * DebuggerServer.addGlobalActor. + * + * @param aFactories + * An object whose own property names are the names of properties to add to + * some reply packet (say, a tab actor grip or the "listTabs" response + * form), and whose own property values are actor constructor functions, as + * documented for addTabActor and addGlobalActor. + * + * @param this + * The BrowserRootActor or BrowserTabActor with which the new actors will + * be associated. It should support whatever API the |aFactories| + * constructor functions might be interested in, as it is passed to them. + * For the sake of CommonCreateExtraActors itself, it should have at least + * the following properties: + * + * - _extraActors + * An object whose own property names are factory table (and packet) + * property names, and whose values are no-argument actor constructors, + * of the sort that one can add to an ActorPool. + * + * - conn + * The DebuggerServerConnection in which the new actors will participate. + * + * - actorID + * The actor's name, for use as the new actors' parentID. + */ +function CommonCreateExtraActors(aFactories, aPool) { + // Walk over global actors added by extensions. + for (let name in aFactories) { + let actor = this._extraActors[name]; + if (!actor) { + actor = aFactories[name].bind(null, this.conn, this); + actor.prototype = aFactories[name].prototype; + actor.parentID = this.actorID; + this._extraActors[name] = actor; + } + aPool.addActor(actor); + } +} + +/** + * Append the extra actors in |this._extraActors|, constructed by a prior call + * to CommonCreateExtraActors, to |aObject|. + * + * @param aObject + * The object to which the extra actors should be added, under the + * property names given in the |aFactories| table passed to + * CommonCreateExtraActors. + * + * @param this + * The BrowserRootActor or BrowserTabActor whose |_extraActors| table we + * should use; see above. + */ +function CommonAppendExtraActors(aObject) { + for (let name in this._extraActors) { + let actor = this._extraActors[name]; + aObject[name] = actor.actorID; + } +} + +/** + * Create a remote debugging protocol root actor. + * + * @param aConnection + * The DebuggerServerConnection whose root actor we are constructing. + * + * @param aParameters + * The properties of |aParameters| provide backing objects for the root + * actor's requests; if a given property is omitted from |aParameters|, the + * root actor won't implement the corresponding requests or notifications. + * Supported properties: + * + * - tabList: a live list (see below) of tab actors. If present, the + * new root actor supports the 'listTabs' request, providing the live + * list's elements as its tab actors, and sending 'tabListChanged' + * notifications when the live list's contents change. One actor in + * this list must have a true '.selected' property. + * + * - globalActorFactories: an object |A| describing further actors to + * attach to the 'listTabs' reply. This is the type accumulated by + * DebuggerServer.addGlobalActor. For each own property |P| of |A|, + * the root actor adds a property named |P| to the 'listTabs' + * reply whose value is the name of an actor constructed by + * |A[P]|. + * + * - onShutdown: a function to call when the root actor is disconnected. + * + * Instance properties: + * + * - applicationType: the string the root actor will include as the + * "applicationType" property in the greeting packet. By default, this + * is "browser". + * + * Live lists: + * + * A "live list", as used for the |tabList|, is an object that presents a + * list of actors, and also notifies its clients of changes to the list. A + * live list's interface is two properties: + * + * - iterator: a method that returns an iterator. A for-of loop will call + * this method to obtain an iterator for the loop, so if LL is + * a live list, one can simply write 'for (i of LL) ...'. + * + * - onListChanged: a handler called, with no arguments, when the set of + * values the iterator would produce has changed since the last + * time 'iterator' was called. This may only be set to null or a + * callable value (one for which the typeof operator returns + * 'function'). (Note that the live list will not call the + * onListChanged handler until the list has been iterated over + * once; if nobody's seen the list in the first place, nobody + * should care if its contents have changed!) + * + * When the list changes, the list implementation should ensure that any + * actors yielded in previous iterations whose referents (tabs) still exist + * get yielded again in subsequent iterations. If the underlying referent + * is the same, the same actor should be presented for it. + * + * The root actor registers an 'onListChanged' handler on the appropriate + * list when it may need to send the client 'tabListChanged' notifications, + * and is careful to remove the handler whenever it does not need to send + * such notifications (including when it is disconnected). This means that + * live list implementations can use the state of the handler property (set + * or null) to install and remove observers and event listeners. + * + * Note that, as the only way for the root actor to see the members of the + * live list is to begin an iteration over the list, the live list need not + * actually produce any actors until they are reached in the course of + * iteration: alliterative lazy live lists. + */ +function RootActor(aConnection, aParameters) { + this.conn = aConnection; + this._parameters = aParameters; + this._onTabListChanged = this.onTabListChanged.bind(this); + this._extraActors = {}; +} + +RootActor.prototype = { + constructor: RootActor, + applicationType: "browser", + + /** + * Return a 'hello' packet as specified by the Remote Debugging Protocol. + */ + sayHello: function() { + return { + from: "root", + applicationType: this.applicationType, + /* This is not in the spec, but it's used by tests. */ + testConnectionPrefix: this.conn.prefix, + traits: { + sources: true + } + }; + }, + + /** + * Disconnects the actor from the browser window. + */ + disconnect: function() { + /* Tell the live lists we aren't watching any more. */ + if (this._parameters.tabList) { + this._parameters.tabList.onListChanged = null; + } + if (typeof this._parameters.onShutdown === 'function') { + this._parameters.onShutdown(); + } + this._extraActors = null; + }, + + /* The 'listTabs' request and the 'tabListChanged' notification. */ + + /** + * Handles the listTabs request. The actors will survive until at least + * the next listTabs request. + */ + onListTabs: function() { + + let tabList = this._parameters.tabList; + if (!tabList) { + return { from: "root", error: "noTabs", + message: "This root actor has no browser tabs." }; + } + + /* + * Walk the tab list, accumulating the array of tab actors for the + * reply, and moving all the actors to a new ActorPool. We'll + * replace the old tab actor pool with the one we build here, thus + * retiring any actors that didn't get listed again, and preparing any + * new actors to receive packets. + */ + let newActorPool = new ActorPool(this.conn); + let tabActorList = []; + let selected; + for (let tabActor of tabList) { + if (tabActor.selected) { + selected = tabActorList.length; + } + tabActor.parentID = this.actorID; + newActorPool.addActor(tabActor); + tabActorList.push(tabActor); + } + + /* DebuggerServer.addGlobalActor support: create actors. */ + this._createExtraActors(this._parameters.globalActorFactories, newActorPool); + + /* + * Drop the old actorID -> actor map. Actors that still mattered were + * added to the new map; others will go away. + */ + if (this._tabActorPool) { + this.conn.removeActorPool(this._tabActorPool); + } + this._tabActorPool = newActorPool; + this.conn.addActorPool(this._tabActorPool); + + let reply = { + "from": "root", + "selected": selected || 0, + "tabs": [actor.grip() for (actor of tabActorList)] + }; + + /* DebuggerServer.addGlobalActor support: name actors in 'listTabs' reply. */ + this._appendExtraActors(reply); + + /* + * Now that we're actually going to report the contents of tabList to + * the client, we're responsible for letting the client know if it + * changes. + */ + tabList.onListChanged = this._onTabListChanged; + + return reply; + }, + + onTabListChanged: function () { + this.conn.send({ from:"root", type:"tabListChanged" }); + /* It's a one-shot notification; no need to watch any more. */ + this._parameters.tabList.onListChanged = null; + }, + + /* This is not in the spec, but it's used by tests. */ + onEcho: (aRequest) => aRequest, + + /* Support for DebuggerServer.addGlobalActor. */ + _createExtraActors: CommonCreateExtraActors, + _appendExtraActors: CommonAppendExtraActors, + + /* ThreadActor hooks. */ + + /** + * Prepare to enter a nested event loop by disabling debuggee events. + */ + preNest: function() { + // Disable events in all open windows. + let e = windowMediator.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suppressEventHandling(true); + windowUtils.suspendTimeouts(); + } + }, + + /** + * Prepare to exit a nested event loop by enabling debuggee events. + */ + postNest: function(aNestData) { + // Enable events in all open windows. + let e = windowMediator.getEnumerator(null); + while (e.hasMoreElements()) { + let win = e.getNext(); + let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.resumeTimeouts(); + windowUtils.suppressEventHandling(false); + } + }, + + /* ChromeDebuggerActor hooks. */ + + /** + * Add the specified actor to the default actor pool connection, in order to + * keep it alive as long as the server is. This is used by breakpoints in the + * thread and chrome debugger actors. + * + * @param actor aActor + * The actor object. + */ + addToParentPool: function(aActor) { + this.conn.addActor(aActor); + }, + + /** + * Remove the specified actor from the default actor pool. + * + * @param BreakpointActor aActor + * The actor object. + */ + removeFromParentPool: function(aActor) { + this.conn.removeActor(aActor); + } +} + +RootActor.prototype.requestTypes = { + "listTabs": RootActor.prototype.onListTabs, + "echo": RootActor.prototype.onEcho +}; diff --git a/cocos/scripting/javascript/bindings/js/debugger/actors/script.js b/cocos/scripting/javascript/bindings/js/debugger/actors/script.js new file mode 100644 index 0000000000..7ddade2b54 --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/actors/script.js @@ -0,0 +1,2948 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * JSD2 actors. + */ +/** + * Creates a ThreadActor. + * + * ThreadActors manage a JSInspector object and manage execution/inspection + * of debuggees. + * + * @param aHooks object + * An object with preNest and postNest methods for calling when entering + * and exiting a nested event loop, addToParentPool and + * removeFromParentPool methods for handling the lifetime of actors that + * will outlive the thread, like breakpoints. + * @param aGlobal object [optional] + * An optional (for content debugging only) reference to the content + * window. + */ +function ThreadActor(aHooks, aGlobal) +{ + this._state = "detached"; + this._frameActors = []; + this._environmentActors = []; + this._hooks = aHooks; + this.global = aGlobal; + + this.findGlobals = this.globalManager.findGlobals.bind(this); + this.onNewGlobal = this.globalManager.onNewGlobal.bind(this); + this.onNewSource = this.onNewSource.bind(this); + + this._options = { + useSourceMaps: false + }; +} + +/** + * The breakpoint store must be shared across instances of ThreadActor so that + * page reloads don't blow away all of our breakpoints. + */ +ThreadActor._breakpointStore = {}; + +ThreadActor.prototype = { + actorPrefix: "context", + + get state() { return this._state; }, + get attached() this.state == "attached" || + this.state == "running" || + this.state == "paused", + + get _breakpointStore() { return ThreadActor._breakpointStore; }, + + get threadLifetimePool() { + if (!this._threadLifetimePool) { + this._threadLifetimePool = new ActorPool(this.conn); + this.conn.addActorPool(this._threadLifetimePool); + this._threadLifetimePool.objectActors = new WeakMap(); + } + return this._threadLifetimePool; + }, + + get sources() { + if (!this._sources) { + this._sources = new ThreadSources(this, this._options.useSourceMaps, + this._allowSource, this.onNewSource); + } + return this._sources; + }, + + clearDebuggees: function TA_clearDebuggees() { + if (this.dbg) { + this.dbg.removeAllDebuggees(); + } + this.conn.removeActorPool(this._threadLifetimePool || undefined); + this._threadLifetimePool = null; + this._sources = null; + }, + + /** + * Add a debuggee global to the Debugger object. + */ + addDebuggee: function TA_addDebuggee(aGlobal) { + try { + this.dbg.addDebuggee(aGlobal); + } catch (e) { + // Ignore attempts to add the debugger's compartment as a debuggee. + dumpn("Ignoring request to add the debugger's compartment as a debuggee"); + } + }, + + /** + * Initialize the Debugger. + */ + _initDebugger: function TA__initDebugger() { + this.dbg = new Debugger(); + this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); + this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this); + this.dbg.onNewScript = this.onNewScript.bind(this); + this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this); + // Keep the debugger disabled until a client attaches. + this.dbg.enabled = this._state != "detached"; + }, + + /** + * Remove a debuggee global from the JSInspector. + */ + removeDebugee: function TA_removeDebuggee(aGlobal) { + try { + this.dbg.removeDebuggee(aGlobal); + } catch(ex) { + // XXX: This debuggee has code currently executing on the stack, + // we need to save this for later. + } + }, + + /** + * Add the provided window and all windows in its frame tree as debuggees. + */ + _addDebuggees: function TA__addDebuggees(aWindow) { + this.addDebuggee(aWindow); + let frames = aWindow.frames; + if (frames) { + for (let i = 0; i < frames.length; i++) { + this._addDebuggees(frames[i]); + } + } + }, + + /** + * An object that will be used by ThreadActors to tailor their behavior + * depending on the debugging context being required (chrome or content). + */ + globalManager: { + findGlobals: function TA_findGlobals() { + this._addDebuggees(this.global); + }, + + /** + * A function that the engine calls when a new global object has been + * created. + * + * @param aGlobal Debugger.Object + * The new global object that was created. + */ + onNewGlobal: function TA_onNewGlobal(aGlobal) { + // Content debugging only cares about new globals in the contant window, + // like iframe children. + if (aGlobal.hostAnnotations && + aGlobal.hostAnnotations.type == "document" && + aGlobal.hostAnnotations.element === this.global) { + this.addDebuggee(aGlobal); + // Notify the client. + this.conn.send({ + from: this.actorID, + type: "newGlobal", + // TODO: after bug 801084 lands see if we need to JSONify this. + hostAnnotations: aGlobal.hostAnnotations + }); + } + } + }, + + disconnect: function TA_disconnect() { + dumpn("in ThreadActor.prototype.disconnect"); + if (this._state == "paused") { + this.onResume(); + } + + this._state = "exited"; + + this.clearDebuggees(); + + if (!this.dbg) { + return; + } + this.dbg.enabled = false; + this.dbg = null; + }, + + /** + * Disconnect the debugger and put the actor in the exited state. + */ + exit: function TA_exit() { + this.disconnect(); + }, + + // Request handlers + onAttach: function TA_onAttach(aRequest) { + if (this.state === "exited") { + return { type: "exited" }; + } + + if (this.state !== "detached") { + return { error: "wrongState" }; + } + + this._state = "attached"; + + update(this._options, aRequest.options || {}); + + if (!this.dbg) { + this._initDebugger(); + } + this.findGlobals(); + this.dbg.enabled = true; + try { + // Put ourselves in the paused state. + let packet = this._paused(); + if (!packet) { + return { error: "notAttached" }; + } + packet.why = { type: "attached" }; + + this._restoreBreakpoints(); + + // Send the response to the attach request now (rather than + // returning it), because we're going to start a nested event loop + // here. + this.conn.send(packet); + + // Start a nested event loop. + this._nest(); + + // We already sent a response to this request, don't send one + // now. + return null; + } catch (e) { + reportError(e); + return { error: "notAttached", message: e.toString() }; + } + }, + + onDetach: function TA_onDetach(aRequest) { + this.disconnect(); + dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet"); + return { + type: "detached" + }; + }, + + onReconfigure: function TA_onReconfigure(aRequest) { + if (this.state == "exited") { + return { error: "wrongState" }; + } + + update(this._options, aRequest.options || {}); + // Clear existing sources, so they can be recreated on next access. + this._sources = null; + + return {}; + }, + + /** + * Pause the debuggee, by entering a nested event loop, and return a 'paused' + * packet to the client. + * + * @param Debugger.Frame aFrame + * The newest debuggee frame in the stack. + * @param object aReason + * An object with a 'type' property containing the reason for the pause. + * @param function onPacket + * Hook to modify the packet before it is sent. Feel free to return a + * promise. + */ + _pauseAndRespond: function TA__pauseAndRespond(aFrame, aReason, + onPacket=function (k) k) { + try { + let packet = this._paused(aFrame); + if (!packet) { + return undefined; + } + packet.why = aReason; + resolve(onPacket(packet)).then(this.conn.send.bind(this.conn)); + return this._nest(); + } catch(e) { + let msg = "Got an exception during TA__pauseAndRespond: " + e + + ": " + e.stack; + // Cu.reportError(msg); + dumpn(msg); + return undefined; + } + }, + + /** + * Handle a protocol request to resume execution of the debuggee. + */ + onResume: function TA_onResume(aRequest) { + if (this._state !== "paused") { + return { + error: "wrongState", + message: "Can't resume when debuggee isn't paused. Current state is '" + + this._state + "'" + }; + } + + // In case of multiple nested event loops (due to multiple debuggers open in + // different tabs or multiple debugger clients connected to the same tab) + // only allow resumption in a LIFO order. + if (DebuggerServer.xpcInspector.eventLoopNestLevel() > 1) { + log("DebuggerServer.xpcInspector.eventLoopNestLevel: "+DebuggerServer.xpcInspector.eventLoopNestLevel()); + // let lastNestRequestor = DebuggerServer.xpcInspector.lastNestRequestor; + // if (lastNestRequestor.connection != this.conn) { + // return { error: "wrongOrder", + // message: "trying to resume in the wrong order.", + // lastPausedUrl: lastNestRequestor.url }; + // } + } + + if (aRequest && aRequest.forceCompletion) { + // TODO: remove this when Debugger.Frame.prototype.pop is implemented in + // bug 736733. + if (typeof this.frame.pop != "function") { + return { error: "notImplemented", + message: "forced completion is not yet implemented." }; + } + + this.dbg.getNewestFrame().pop(aRequest.completionValue); + let packet = this._resumed(); + DebuggerServer.xpcInspector.exitNestedEventLoop(); + return { type: "resumeLimit", frameFinished: aRequest.forceCompletion }; + } + + if (aRequest && aRequest.resumeLimit) { + log("resumeLimit..."); + // Bind these methods because some of the hooks are called with 'this' + // set to the current frame. + let pauseAndRespond = (aFrame, onPacket=function (k) k) => { + this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket); + }; + let createValueGrip = this.createValueGrip.bind(this); + + let startFrame = this.youngestFrame; + let startLine; + if (this.youngestFrame.script) { + let offset = this.youngestFrame.offset; + startLine = this.youngestFrame.script.getOffsetLine(offset); + } + + // Define the JS hook functions for stepping. + + let onEnterFrame = aFrame => { + if (this.sources.isBlackBoxed(aFrame.script.url)) { + return undefined; + } + return pauseAndRespond(aFrame); + }; + + let thread = this; + + let onPop = function TA_onPop(aCompletion) { + // onPop is called with 'this' set to the current frame. + if (thread.sources.isBlackBoxed(this.script.url)) { + return undefined; + } + + // Note that we're popping this frame; we need to watch for + // subsequent step events on its caller. + this.reportedPop = true; + + return pauseAndRespond(this, (aPacket) => { + aPacket.why.frameFinished = {}; + if (!aCompletion) { + aPacket.why.frameFinished.terminated = true; + } else if (aCompletion.hasOwnProperty("return")) { + aPacket.why.frameFinished.return = createValueGrip(aCompletion.return); + } else if (aCompletion.hasOwnProperty("yield")) { + aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield); + } else { + aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw); + } + return aPacket; + }); + }; + + let onStep = function TA_onStep() { + // onStep is called with 'this' set to the current frame. + + if (thread.sources.isBlackBoxed(this.script.url)) { + return undefined; + } + + // If we've changed frame or line, then report that. + if (this !== startFrame || + (this.script && + this.script.getOffsetLine(this.offset) != startLine)) { + return pauseAndRespond(this); + } + + // Otherwise, let execution continue. + return undefined; + }; + + let steppingType = aRequest.resumeLimit.type; + if (["step", "next", "finish"].indexOf(steppingType) == -1) { + return { error: "badParameterType", + message: "Unknown resumeLimit type" }; + } + // Make sure there is still a frame on the stack if we are to continue + // stepping. + let stepFrame = this._getNextStepFrame(startFrame); + if (stepFrame) { + switch (steppingType) { + case "step": + log("--> step..."); + this.dbg.onEnterFrame = onEnterFrame; + // Fall through. + case "next": + log("--> next..."); + stepFrame.onStep = onStep; + stepFrame.onPop = onPop; + break; + case "finish": + log("--> finish..."); + stepFrame.onPop = onPop; + } + } + + } + + if (aRequest && aRequest.pauseOnExceptions) { + this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this); + } + let packet = this._resumed(); + DebuggerServer.xpcInspector.exitNestedEventLoop(); + return packet; + }, + + /** + * Helper method that returns the next frame when stepping. + */ + _getNextStepFrame: function TA__getNextStepFrame(aFrame) { + let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame; + if (!stepFrame || !stepFrame.script) { + stepFrame = null; + } + return stepFrame; + }, + + onClientEvaluate: function TA_onClientEvaluate(aRequest) { + if (this.state !== "paused") { + return { error: "wrongState", + message: "Debuggee must be paused to evaluate code." }; + }; + + let frame = this._requestFrame(aRequest.frame); + if (!frame) { + return { error: "unknownFrame", + message: "Evaluation frame not found" }; + } + + if (!frame.environment) { + return { error: "notDebuggee", + message: "cannot access the environment of this frame." }; + }; + + // We'll clobber the youngest frame if the eval causes a pause, so + // save our frame now to be restored after eval returns. + // XXX: or we could just start using dbg.getNewestFrame() now that it + // works as expected. + let youngest = this.youngestFrame; + + // Put ourselves back in the running state and inform the client. + let resumedPacket = this._resumed(); + this.conn.send(resumedPacket); + + // Run the expression. + // XXX: test syntax errors + let completion = frame.eval(aRequest.expression); + + // Put ourselves back in the pause state. + let packet = this._paused(youngest); + packet.why = { type: "clientEvaluated", + frameFinished: this.createProtocolCompletionValue(completion) }; + + // Return back to our previous pause's event loop. + return packet; + }, + + onFrames: function TA_onFrames(aRequest) { + if (this.state !== "paused") { + return { error: "wrongState", + message: "Stack frames are only available while the debuggee is paused."}; + } + + let start = aRequest.start ? aRequest.start : 0; + let count = aRequest.count; + + // Find the starting frame... + let frame = this.youngestFrame; + let i = 0; + while (frame && (i < start)) { + frame = frame.older; + i++; + } + + // Return request.count frames, or all remaining + // frames if count is not defined. + let frames = []; + let promises = []; + for (; frame && (!count || i < (start + count)); i++, frame=frame.older) { + let form = this._createFrameActor(frame).form(); + form.depth = i; + frames.push(form); + + let promise = this.sources.getOriginalLocation(form.where.url, + form.where.line) + .then(function (aOrigLocation) { + form.where = aOrigLocation; + }); + promises.push(promise); + } + + return all(promises).then(function () { + return { frames: frames }; + }); + }, + + onReleaseMany: function TA_onReleaseMany(aRequest) { + if (!aRequest.actors) { + return { error: "missingParameter", + message: "no actors were specified" }; + } + + let res; + for each (let actorID in aRequest.actors) { + let actor = this.threadLifetimePool.get(actorID); + if (!actor) { + if (!res) { + res = { error: "notReleasable", + message: "Only thread-lifetime actors can be released." }; + } + continue; + } + actor.onRelease(); + } + return res ? res : {}; + }, + + /** + * Handle a protocol request to set a breakpoint. + */ + onSetBreakpoint: function TA_onSetBreakpoint(aRequest) { + if (this.state !== "paused") { + return { error: "wrongState", + message: "Breakpoints can only be set while the debuggee is paused."}; + } + + // XXX: `originalColumn` is never used. See bug 827639. + let { url: originalSource, + line: originalLine, + column: originalColumn } = aRequest.location; + + let locationPromise = this.sources.getGeneratedLocation(originalSource, + originalLine) + return locationPromise.then((aLocation) => { + let line = aLocation.line; + if (this.dbg.findScripts({ url: aLocation.url }).length == 0 || + line < 0 || + line == null) { + return { error: "noScript" }; + } + + // Add the breakpoint to the store for later reuse, in case it belongs to a + // script that hasn't appeared yet. + if (!this._breakpointStore[aLocation.url]) { + this._breakpointStore[aLocation.url] = []; + } + let scriptBreakpoints = this._breakpointStore[aLocation.url]; + scriptBreakpoints[line] = { + url: aLocation.url, + line: line, + column: aLocation.column + }; + + let response = this._setBreakpoint(aLocation); + // If the original location of our generated location is different from + // the original location we attempted to set the breakpoint on, we will + // need to know so that we can set actualLocation on the response. + let originalLocation = this.sources.getOriginalLocation(aLocation.url, + aLocation.line); + + return all([response, originalLocation]) + .then(([aResponse, {url, line}]) => { + if (aResponse.actualLocation) { + let actualOrigLocation = this.sources.getOriginalLocation( + aResponse.actualLocation.url, aResponse.actualLocation.line); + return actualOrigLocation.then(function ({ url, line }) { + if (url !== originalSource || line !== originalLine) { + aResponse.actualLocation = { url: url, line: line }; + } + return aResponse; + }); + } + + if (url !== originalSource || line !== originalLine) { + aResponse.actualLocation = { url: url, line: line }; + } + + return aResponse; + }); + }); + }, + + /** + * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint + * is being set contains no code, then the breakpoint will slide down to the + * next line that has runnable code. In this case the server breakpoint cache + * will be updated, so callers that iterate over the breakpoint cache should + * take that into account. + * + * @param object aLocation + * The location of the breakpoint as specified in the protocol. + */ + _setBreakpoint: function TA__setBreakpoint(aLocation) { + let breakpoints = this._breakpointStore[aLocation.url]; + + // Get or create the breakpoint actor for the given location + let actor; + if (breakpoints[aLocation.line].actor) { + actor = breakpoints[aLocation.line].actor; + } else { + actor = breakpoints[aLocation.line].actor = new BreakpointActor(this, { + url: aLocation.url, + line: aLocation.line + }); + this._hooks.addToParentPool(actor); + } + + // Find all scripts matching the given location + let scripts = this.dbg.findScripts(aLocation); + if (scripts.length == 0) { + return { + error: "noScript", + actor: actor.actorID + }; + } + + /** + * For each script, if the given line has at least one entry point, set + * breakpoint on the bytecode offet for each of them. + */ + let found = false; + for (let script of scripts) { + let offsets = script.getLineOffsets(aLocation.line); + if (offsets.length > 0) { + for (let offset of offsets) { + script.setBreakpoint(offset, actor); + } + actor.addScript(script, this); + found = true; + } + } + if (found) { + return { + actor: actor.actorID + }; + } + + /** + * If we get here, no breakpoint was set. This is because the given line + * has no entry points, for example because it is empty. As a fallback + * strategy, we try to set the breakpoint on the smallest line greater + * than or equal to the given line that as at least one entry point. + */ + + // Find all innermost scripts matching the given location + let scripts = this.dbg.findScripts({ + url: aLocation.url, + line: aLocation.line, + innermost: true + }); + + /** + * For each innermost script, look for the smallest line greater than or + * equal to the given line that has one or more entry points. If found, set + * a breakpoint on the bytecode offset for each of its entry points. + */ + let actualLocation; + let found = false; + for (let script of scripts) { + let offsets = script.getAllOffsets(); + for (let line = aLocation.line; line < offsets.length; ++line) { + if (offsets[line]) { + for (let offset of offsets[line]) { + script.setBreakpoint(offset, actor); + } + actor.addScript(script, this); + if (!actualLocation) { + actualLocation = { + url: aLocation.url, + line: line, + column: 0 + }; + } + found = true; + break; + } + } + } + if (found) { + if (breakpoints[actualLocation.line] && + breakpoints[actualLocation.line].actor) { + /** + * We already have a breakpoint actor for the actual location, so + * actor we created earlier is now redundant. Delete it, update the + * breakpoint store, and return the actor for the actual location. + */ + actor.onDelete(); + delete breakpoints[aLocation.line]; + return { + actor: breakpoints[actualLocation.line].actor.actorID, + actualLocation: actualLocation + }; + } else { + /** + * We don't have a breakpoint actor for the actual location yet. + * Instead or creating a new actor, reuse the actor we created earlier, + * and update the breakpoint store. + */ + actor.location = actualLocation; + breakpoints[actualLocation.line] = breakpoints[aLocation.line]; + delete breakpoints[aLocation.line]; + // WARNING: This overwrites aLocation.line + breakpoints[actualLocation.line].line = actualLocation.line; + return { + actor: actor.actorID, + actualLocation: actualLocation + }; + } + } + + /** + * If we get here, no line matching the given line was found, so just + * epically. + */ + return { + error: "noCodeAtLineColumn", + actor: actor.actorID + }; + }, + + /** + * Get the script and source lists from the debugger. + * + * TODO bug 637572: we should be dealing with sources directly, not inferring + * them through scripts. + */ + _discoverSources: function TA__discoverSources() { + // Only get one script per url. + let scriptsByUrl = {}; + for (let s of this.dbg.findScripts()) { + scriptsByUrl[s.url] = s; + } + + return all([this.sources.sourcesForScript(scriptsByUrl[s]) + for (s of Object.keys(scriptsByUrl))]); + }, + + onSources: function TA_onSources(aRequest) { + return this._discoverSources().then(() => { + return { + sources: [s.form() for (s of this.sources.iter())] + }; + }); + }, + + /** + * Disassociate all breakpoint actors from their scripts and clear the + * breakpoint handlers. This method can be used when the thread actor intends + * to keep the breakpoint store, but needs to clear any actual breakpoints, + * e.g. due to a page navigation. This way the breakpoint actors' script + * caches won't hold on to the Debugger.Script objects leaking memory. + */ + disableAllBreakpoints: function () { + for (let url in this._breakpointStore) { + for (let line in this._breakpointStore[url]) { + let bp = this._breakpointStore[url][line]; + bp.actor.removeScripts(); + } + } + }, + + /** + * Handle a protocol request to pause the debuggee. + */ + onInterrupt: function TA_onInterrupt(aRequest) { + if (this.state == "exited") { + return { type: "exited" }; + } else if (this.state == "paused") { + // TODO: return the actual reason for the existing pause. + return { type: "paused", why: { type: "alreadyPaused" } }; + } else if (this.state != "running") { + return { error: "wrongState", + message: "Received interrupt request in " + this.state + + " state." }; + } + + try { + // Put ourselves in the paused state. + let packet = this._paused(); + if (!packet) { + return { error: "notInterrupted" }; + } + packet.why = { type: "interrupted" }; + + // Send the response to the interrupt request now (rather than + // returning it), because we're going to start a nested event loop + // here. + this.conn.send(packet); + + // Start a nested event loop. + this._nest(); + + // We already sent a response to this request, don't send one + // now. + return null; + } catch (e) { + reportError(e); + return { error: "notInterrupted", message: e.toString() }; + } + }, + + /** + * Return the Debug.Frame for a frame mentioned by the protocol. + */ + _requestFrame: function TA_requestFrame(aFrameID) { + if (!aFrameID) { + return this.youngestFrame; + } + + if (this._framePool.has(aFrameID)) { + return this._framePool.get(aFrameID).frame; + } + + return undefined; + }, + + _paused: function TA_paused(aFrame) { + // We don't handle nested pauses correctly. Don't try - if we're + // paused, just continue running whatever code triggered the pause. + // We don't want to actually have nested pauses (although we + // have nested event loops). If code runs in the debuggee during + // a pause, it should cause the actor to resume (dropping + // pause-lifetime actors etc) and then repause when complete. + + if (this.state === "paused") { + return undefined; + } + + // Clear stepping hooks. + this.dbg.onEnterFrame = undefined; + this.dbg.onExceptionUnwind = undefined; + if (aFrame) { + aFrame.onStep = undefined; + aFrame.onPop = undefined; + } + + this._state = "paused"; + + // Save the pause frame (if any) as the youngest frame for + // stack viewing. + this.youngestFrame = aFrame; + + // Create the actor pool that will hold the pause actor and its + // children. + dbg_assert(!this._pausePool); + this._pausePool = new ActorPool(this.conn); + this.conn.addActorPool(this._pausePool); + + // Give children of the pause pool a quick link back to the + // thread... + this._pausePool.threadActor = this; + + // Create the pause actor itself... + dbg_assert(!this._pauseActor); + this._pauseActor = new PauseActor(this._pausePool); + this._pausePool.addActor(this._pauseActor); + + // Update the list of frames. + let poppedFrames = this._updateFrames(); + + // Send off the paused packet and spin an event loop. + let packet = { from: this.actorID, + type: "paused", + actor: this._pauseActor.actorID }; + if (aFrame) { + packet.frame = this._createFrameActor(aFrame).form(); + } + + if (poppedFrames) { + packet.poppedFrames = poppedFrames; + } + + return packet; + }, + + _nest: function TA_nest() { + if (this._hooks.preNest) { + var nestData = this._hooks.preNest(); + } + + let requestor = Object.create(null); + requestor.url = this._hooks.url; + requestor.connection = this.conn; + DebuggerServer.xpcInspector.enterNestedEventLoop(requestor); + + dbg_assert(this.state === "running"); + + if (this._hooks.postNest) { + this._hooks.postNest(nestData) + } + + // "continue" resumption value. + return undefined; + }, + + _resumed: function TA_resumed() { + this._state = "running"; + + // Drop the actors in the pause actor pool. + this.conn.removeActorPool(this._pausePool); + + this._pausePool = null; + this._pauseActor = null; + this.youngestFrame = null; + + return { from: this.actorID, type: "resumed" }; + }, + + /** + * Expire frame actors for frames that have been popped. + * + * @returns A list of actor IDs whose frames have been popped. + */ + _updateFrames: function TA_updateFrames() { + let popped = []; + + // Create the actor pool that will hold the still-living frames. + let framePool = new ActorPool(this.conn); + let frameList = []; + + for each (let frameActor in this._frameActors) { + if (frameActor.frame.live) { + framePool.addActor(frameActor); + frameList.push(frameActor); + } else { + popped.push(frameActor.actorID); + } + } + + // Remove the old frame actor pool, this will expire + // any actors that weren't added to the new pool. + if (this._framePool) { + this.conn.removeActorPool(this._framePool); + } + + this._frameActors = frameList; + this._framePool = framePool; + this.conn.addActorPool(framePool); + + return popped; + }, + + _createFrameActor: function TA_createFrameActor(aFrame) { + if (aFrame.actor) { + return aFrame.actor; + } + + let actor = new FrameActor(aFrame, this); + this._frameActors.push(actor); + this._framePool.addActor(actor); + aFrame.actor = actor; + + return actor; + }, + + /** + * Create and return an environment actor that corresponds to the provided + * Debugger.Environment. + * @param Debugger.Environment aEnvironment + * The lexical environment we want to extract. + * @param object aPool + * The pool where the newly-created actor will be placed. + * @return The EnvironmentActor for aEnvironment or undefined for host + * functions or functions scoped to a non-debuggee global. + */ + createEnvironmentActor: + function TA_createEnvironmentActor(aEnvironment, aPool) { + if (!aEnvironment) { + return undefined; + } + + if (aEnvironment.actor) { + return aEnvironment.actor; + } + + let actor = new EnvironmentActor(aEnvironment, this); + this._environmentActors.push(actor); + aPool.addActor(actor); + aEnvironment.actor = actor; + + return actor; + }, + + /** + * Create a grip for the given debuggee value. If the value is an + * object, will create an actor with the given lifetime. + */ + createValueGrip: function TA_createValueGrip(aValue, aPool=false) { + if (!aPool) { + aPool = this._pausePool; + } + let type = typeof(aValue); + + if (type === "string" && this._stringIsLong(aValue)) { + return this.longStringGrip(aValue, aPool); + } + + if (type === "boolean" || type === "string" || type === "number") { + return aValue; + } + + if (aValue === null) { + return { type: "null" }; + } + + if (aValue === undefined) { + return { type: "undefined" } + } + + if (typeof(aValue) === "object") { + return this.objectGrip(aValue, aPool); + } + + dbg_assert(false, "Failed to provide a grip for: " + aValue); + return null; + }, + + /** + * Return a protocol completion value representing the given + * Debugger-provided completion value. + */ + createProtocolCompletionValue: + function TA_createProtocolCompletionValue(aCompletion) { + let protoValue = {}; + if ("return" in aCompletion) { + protoValue.return = this.createValueGrip(aCompletion.return); + } else if ("yield" in aCompletion) { + protoValue.return = this.createValueGrip(aCompletion.yield); + } else if ("throw" in aCompletion) { + protoValue.throw = this.createValueGrip(aCompletion.throw); + } else { + protoValue.terminated = true; + } + return protoValue; + }, + + /** + * Create a grip for the given debuggee object. + * + * @param aValue Debugger.Object + * The debuggee object value. + * @param aPool ActorPool + * The actor pool where the new object actor will be added. + */ + objectGrip: function TA_objectGrip(aValue, aPool) { + if (!aPool.objectActors) { + aPool.objectActors = new WeakMap(); + } + + if (aPool.objectActors.has(aValue)) { + return aPool.objectActors.get(aValue).grip(); + } else if (this.threadLifetimePool.objectActors.has(aValue)) { + return this.threadLifetimePool.objectActors.get(aValue).grip(); + } + + let actor = new PauseScopedObjectActor(aValue, this); + aPool.addActor(actor); + aPool.objectActors.set(aValue, actor); + return actor.grip(); + }, + + /** + * Create a grip for the given debuggee object with a pause lifetime. + * + * @param aValue Debugger.Object + * The debuggee object value. + */ + pauseObjectGrip: function TA_pauseObjectGrip(aValue) { + if (!this._pausePool) { + throw "Object grip requested while not paused."; + } + + return this.objectGrip(aValue, this._pausePool); + }, + + /** + * Extend the lifetime of the provided object actor to thread lifetime. + * + * @param aActor object + * The object actor. + */ + threadObjectGrip: function TA_threadObjectGrip(aActor) { + // We want to reuse the existing actor ID, so we just remove it from the + // current pool's weak map and then let pool.addActor do the rest. + aActor.registeredPool.objectActors.delete(aActor.obj); + this.threadLifetimePool.addActor(aActor); + this.threadLifetimePool.objectActors.set(aActor.obj, aActor); + }, + + /** + * Handle a protocol request to promote multiple pause-lifetime grips to + * thread-lifetime grips. + * + * @param aRequest object + * The protocol request object. + */ + onThreadGrips: function OA_onThreadGrips(aRequest) { + if (this.state != "paused") { + return { error: "wrongState" }; + } + + if (!aRequest.actors) { + return { error: "missingParameter", + message: "no actors were specified" }; + } + + for (let actorID of aRequest.actors) { + let actor = this._pausePool.get(actorID); + if (actor) { + this.threadObjectGrip(actor); + } + } + return {}; + }, + + /** + * Create a grip for the given string. + * + * @param aString String + * The string we are creating a grip for. + * @param aPool ActorPool + * The actor pool where the new actor will be added. + */ + longStringGrip: function TA_longStringGrip(aString, aPool) { + if (!aPool.longStringActors) { + aPool.longStringActors = {}; + } + + if (aPool.longStringActors.hasOwnProperty(aString)) { + return aPool.longStringActors[aString].grip(); + } + + let actor = new LongStringActor(aString, this); + aPool.addActor(actor); + aPool.longStringActors[aString] = actor; + return actor.grip(); + }, + + /** + * Create a long string grip that is scoped to a pause. + * + * @param aString String + * The string we are creating a grip for. + */ + pauseLongStringGrip: function TA_pauseLongStringGrip (aString) { + return this.longStringGrip(aString, this._pausePool); + }, + + /** + * Create a long string grip that is scoped to a thread. + * + * @param aString String + * The string we are creating a grip for. + */ + threadLongStringGrip: function TA_pauseLongStringGrip (aString) { + return this.longStringGrip(aString, this._threadLifetimePool); + }, + + /** + * Returns true if the string is long enough to use a LongStringActor instead + * of passing the value directly over the protocol. + * + * @param aString String + * The string we are checking the length of. + */ + _stringIsLong: function TA__stringIsLong(aString) { + return aString.length >= DebuggerServer.LONG_STRING_LENGTH; + }, + + // JS Debugger API hooks. + + /** + * A function that the engine calls when a call to a debug event hook, + * breakpoint handler, watchpoint handler, or similar function throws some + * exception. + * + * @param aException exception + * The exception that was thrown in the debugger code. + */ + uncaughtExceptionHook: function TA_uncaughtExceptionHook(aException) { + dumpn("Got an exception: " + aException.message + "\n" + aException.stack); + }, + + /** + * A function that the engine calls when a debugger statement has been + * executed in the specified frame. + * + * @param aFrame Debugger.Frame + * The stack frame that contained the debugger statement. + */ + onDebuggerStatement: function TA_onDebuggerStatement(aFrame) { + if (this.sources.isBlackBoxed(aFrame.script.url)) { + return undefined; + } + return this._pauseAndRespond(aFrame, { type: "debuggerStatement" }); + }, + + /** + * A function that the engine calls when an exception has been thrown and has + * propagated to the specified frame. + * + * @param aFrame Debugger.Frame + * The youngest remaining stack frame. + * @param aValue object + * The exception that was thrown. + */ + onExceptionUnwind: function TA_onExceptionUnwind(aFrame, aValue) { + if (this.sources.isBlackBoxed(aFrame.script.url)) { + return undefined; + } + try { + let packet = this._paused(aFrame); + if (!packet) { + return undefined; + } + + packet.why = { type: "exception", + exception: this.createValueGrip(aValue) }; + this.conn.send(packet); + return this._nest(); + } catch(e) { + log("Got an exception during TA_onExceptionUnwind: " + e + + ": " + e.stack); + return undefined; + } + }, + + /** + * A function that the engine calls when a new script has been loaded into the + * scope of the specified debuggee global. + * + * @param aScript Debugger.Script + * The source script that has been loaded into a debuggee compartment. + * @param aGlobal Debugger.Object + * A Debugger.Object instance whose referent is the global object. + */ + onNewScript: function TA_onNewScript(aScript, aGlobal) { + this._addScript(aScript); + this.sources.sourcesForScript(aScript); + }, + + onNewSource: function TA_onNewSource(aSource) { + this.conn.send({ + from: this.actorID, + type: "newSource", + source: aSource.form() + }); + }, + + /** + * Check if scripts from the provided source URL are allowed to be stored in + * the cache. + * + * @param aSourceUrl String + * The url of the script's source that will be stored. + * @returns true, if the script can be added, false otherwise. + */ + _allowSource: function TA__allowSource(aSourceUrl) { + // Ignore anything we don't have a URL for (eval scripts, for example). + if (!aSourceUrl) + return false; + // Ignore XBL bindings for content debugging. + if (aSourceUrl.indexOf("chrome://") == 0) { + return false; + } + // Ignore about:* pages for content debugging. + if (aSourceUrl.indexOf("about:") == 0) { + return false; + } + return true; + }, + + /** + * Restore any pre-existing breakpoints to the scripts that we have access to. + */ + _restoreBreakpoints: function TA__restoreBreakpoints() { + for (let s of this.dbg.findScripts()) { + this._addScript(s); + } + }, + + /** + * Add the provided script to the server cache. + * + * @param aScript Debugger.Script + * The source script that will be stored. + * @returns true, if the script was added; false otherwise. + */ + _addScript: function TA__addScript(aScript) { + if (!this._allowSource(aScript.url)) { + return false; + } + + // Set any stored breakpoints. + let existing = this._breakpointStore[aScript.url]; + if (existing) { + let endLine = aScript.startLine + aScript.lineCount - 1; + // Iterate over the lines backwards, so that sliding breakpoints don't + // affect the loop. + for (let line = existing.length - 1; line >= aScript.startLine; line--) { + let bp = existing[line]; + // Only consider breakpoints that are not already associated with + // scripts, and limit search to the line numbers contained in the new + // script. + if (bp && !bp.actor.scripts.length && line <= endLine) { + this._setBreakpoint(bp); + } + } + } + return true; + }, + +}; + +ThreadActor.prototype.requestTypes = { + "attach": ThreadActor.prototype.onAttach, + "detach": ThreadActor.prototype.onDetach, + "reconfigure": ThreadActor.prototype.onReconfigure, + "resume": ThreadActor.prototype.onResume, + "clientEvaluate": ThreadActor.prototype.onClientEvaluate, + "frames": ThreadActor.prototype.onFrames, + "interrupt": ThreadActor.prototype.onInterrupt, + "releaseMany": ThreadActor.prototype.onReleaseMany, + "setBreakpoint": ThreadActor.prototype.onSetBreakpoint, + "sources": ThreadActor.prototype.onSources, + "threadGrips": ThreadActor.prototype.onThreadGrips +}; + + +/** + * Creates a PauseActor. + * + * PauseActors exist for the lifetime of a given debuggee pause. Used to + * scope pause-lifetime grips. + * + * @param ActorPool aPool + * The actor pool created for this pause. + */ +function PauseActor(aPool) +{ + this.pool = aPool; +} + +PauseActor.prototype = { + actorPrefix: "pause" +}; + + +/** + * A base actor for any actors that should only respond receive messages in the + * paused state. Subclasses may expose a `threadActor` which is used to help + * determine when we are in a paused state. Subclasses should set their own + * "constructor" property if they want better error messages. You should never + * instantiate a PauseScopedActor directly, only through subclasses. + */ +function PauseScopedActor() +{ +} + +/** + * A function decorator for creating methods to handle protocol messages that + * should only be received while in the paused state. + * + * @param aMethod Function + * The function we are decorating. + */ +PauseScopedActor.withPaused = function PSA_withPaused(aMethod) { + return function () { + if (this.isPaused()) { + return aMethod.apply(this, arguments); + } else { + return this._wrongState(); + } + }; +}; + +PauseScopedActor.prototype = { + + /** + * Returns true if we are in the paused state. + */ + isPaused: function PSA_isPaused() { + // When there is not a ThreadActor available (like in the webconsole) we + // have to be optimistic and assume that we are paused so that we can + // respond to requests. + return this.threadActor ? this.threadActor.state === "paused" : true; + }, + + /** + * Returns the wrongState response packet for this actor. + */ + _wrongState: function PSA_wrongState() { + return { + error: "wrongState", + message: this.constructor.name + + " actors can only be accessed while the thread is paused." + }; + } +}; + + +/** + * A SourceActor provides information about the source of a script. + * + * @param aUrl String + * The url of the source we are representing. + * @param aThreadActor ThreadActor + * The current thread actor. + * @param aSourceMap SourceMapConsumer + * Optional. The source map that introduced this source, if available. + */ +function SourceActor(aUrl, aThreadActor, aSourceMap=null) { + this._threadActor = aThreadActor; + this._url = aUrl; + this._sourceMap = aSourceMap; +} + +SourceActor.prototype = { + constructor: SourceActor, + actorPrefix: "source", + + get threadActor() this._threadActor, + get url() this._url, + + form: function SA_form() { + return { + actor: this.actorID, + url: this._url, + isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url) + // TODO bug 637572: introductionScript + }; + }, + + disconnect: function LSA_disconnect() { + if (this.registeredPool && this.registeredPool.sourceActors) { + delete this.registeredPool.sourceActors[this.actorID]; + } + }, + + /** + * Handler for the "source" packet. + */ + onSource: function SA_onSource(aRequest) { + let sourceContent = null; + if (this._sourceMap) { + sourceContent = this._sourceMap.sourceContentFor(this._url); + } + + if (sourceContent) { + return { + from: this.actorID, + source: this.threadActor.createValueGrip( + sourceContent, this.threadActor.threadLifetimePool) + }; + } + + // XXX bug 865252: Don't load from the cache if this is a source mapped + // source because we can't guarantee that the cache has the most up to date + // content for this source like we can if it isn't source mapped. + return fetch(this._url, { loadFromCache: !this._sourceMap }) + .then((aSource) => { + return this.threadActor.createValueGrip( + aSource, this.threadActor.threadLifetimePool); + }) + .then((aSourceGrip) => { + return { + from: this.actorID, + source: aSourceGrip + }; + }, (aError) => { + let msg = "Got an exception during SA_onSource: " + aError + + "\n" + aError.stack; + // Cu.reportError(msg); + dumpn(msg); + return { + "from": this.actorID, + "error": "loadSourceError", + "message": "Could not load the source for " + this._url + "." + }; + }); + }, + + /** + * Handler for the "blackbox" packet. + */ + onBlackBox: function SA_onBlackBox(aRequest) { + this.threadActor.sources.blackBox(this.url); + let packet = { + from: this.actorID + }; + if (this.threadActor.state == "paused" + && this.threadActor.youngestFrame + && this.threadActor.youngestFrame.script.url == this.url) { + packet.pausedInSource = true; + } + return packet; + }, + + /** + * Handler for the "unblackbox" packet. + */ + onUnblackBox: function SA_onUnblackBox(aRequest) { + this.threadActor.sources.unblackBox(this.url); + return { + from: this.actorID + }; + } +}; + +SourceActor.prototype.requestTypes = { + "source": SourceActor.prototype.onSource, + "blackbox": SourceActor.prototype.onBlackBox, + "unblackbox": SourceActor.prototype.onUnblackBox +}; + + +/** + * Creates an actor for the specified object. + * + * @param aObj Debugger.Object + * The debuggee object. + * @param aThreadActor ThreadActor + * The parent thread actor for this object. + */ +function ObjectActor(aObj, aThreadActor) +{ + this.obj = aObj; + this.threadActor = aThreadActor; +} + +ObjectActor.prototype = { + actorPrefix: "obj", + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function OA_grip() { + let g = { + "type": "object", + "class": this.obj.class, + "actor": this.actorID, + "extensible": this.obj.isExtensible(), + "frozen": this.obj.isFrozen(), + "sealed": this.obj.isSealed() + }; + + // Add additional properties for functions. + if (this.obj.class === "Function") { + if (this.obj.name) { + g.name = this.obj.name; + } else if (this.obj.displayName) { + g.displayName = this.obj.displayName; + } + + // Check if the developer has added a de-facto standard displayName + // property for us to use. + let desc = this.obj.getOwnPropertyDescriptor("displayName"); + if (desc && desc.value && typeof desc.value == "string") { + g.userDisplayName = this.threadActor.createValueGrip(desc.value); + } + } + + return g; + }, + + /** + * Releases this actor from the pool. + */ + release: function OA_release() { + if (this.registeredPool.objectActors) { + this.registeredPool.objectActors.delete(this.obj); + } + this.registeredPool.removeActor(this); + }, + + /** + * Handle a protocol request to provide the names of the properties defined on + * the object and not its prototype. + * + * @param aRequest object + * The protocol request object. + */ + onOwnPropertyNames: function OA_onOwnPropertyNames(aRequest) { + return { from: this.actorID, + ownPropertyNames: this.obj.getOwnPropertyNames() }; + }, + + /** + * Handle a protocol request to provide the prototype and own properties of + * the object. + * + * @param aRequest object + * The protocol request object. + */ + onPrototypeAndProperties: function OA_onPrototypeAndProperties(aRequest) { + let ownProperties = Object.create(null); + let names; + try { + names = this.obj.getOwnPropertyNames(); + } catch (ex) { + // The above can throw if this.obj points to a dead object. + // TODO: we should use Cu.isDeadWrapper() - see bug 885800. + return { from: this.actorID, + prototype: this.threadActor.createValueGrip(null), + ownProperties: ownProperties, + safeGetterValues: Object.create(null) }; + } + for (let name of names) { + ownProperties[name] = this._propertyDescriptor(name); + } + return { from: this.actorID, + prototype: this.threadActor.createValueGrip(this.obj.proto), + ownProperties: ownProperties, + safeGetterValues: this._findSafeGetterValues(ownProperties) }; + }, + + /** + * Find the safe getter values for the current Debugger.Object, |this.obj|. + * + * @private + * @param object aOwnProperties + * The object that holds the list of known ownProperties for + * |this.obj|. + * @return object + * An object that maps property names to safe getter descriptors as + * defined by the remote debugging protocol. + */ + _findSafeGetterValues: function OA__findSafeGetterValues(aOwnProperties) + { + let safeGetterValues = Object.create(null); + let obj = this.obj; + let level = 0; + + while (obj) { + let getters = this._findSafeGetters(obj); + for (let name of getters) { + // Avoid overwriting properties from prototypes closer to this.obj. Also + // avoid providing safeGetterValues from prototypes if property |name| + // is already defined as an own property. + if (name in safeGetterValues || + (obj != this.obj && name in aOwnProperties)) { + continue; + } + + let desc = null, getter = null; + try { + desc = obj.getOwnPropertyDescriptor(name); + getter = desc.get; + } catch (ex) { + // The above can throw if the cache becomes stale. + } + if (!getter) { + obj._safeGetters = null; + continue; + } + + let result = getter.call(this.obj); + if (result && !("throw" in result)) { + let getterValue = undefined; + if ("return" in result) { + getterValue = result.return; + } else if ("yield" in result) { + getterValue = result.yield; + } + // WebIDL attributes specified with the LenientThis extended attribute + // return undefined and should be ignored. + if (getterValue !== undefined) { + safeGetterValues[name] = { + getterValue: this.threadActor.createValueGrip(getterValue), + getterPrototypeLevel: level, + enumerable: desc.enumerable, + writable: level == 0 ? desc.writable : true, + }; + } + } + } + + obj = obj.proto; + level++; + } + + return safeGetterValues; + }, + + /** + * Find the safe getters for a given Debugger.Object. Safe getters are native + * getters which are safe to execute. + * + * @private + * @param Debugger.Object aObject + * The Debugger.Object where you want to find safe getters. + * @return Set + * A Set of names of safe getters. This result is cached for each + * Debugger.Object. + */ + _findSafeGetters: function OA__findSafeGetters(aObject) + { + if (aObject._safeGetters) { + return aObject._safeGetters; + } + + let getters = new Set(); + for (let name of aObject.getOwnPropertyNames()) { + let desc = null; + try { + desc = aObject.getOwnPropertyDescriptor(name); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). + } + if (!desc || desc.value !== undefined || !("get" in desc)) { + continue; + } + + let fn = desc.get; + if (fn && fn.callable && fn.class == "Function" && + fn.script === undefined) { + getters.add(name); + } + } + + aObject._safeGetters = getters; + return getters; + }, + + /** + * Handle a protocol request to provide the prototype of the object. + * + * @param aRequest object + * The protocol request object. + */ + onPrototype: function OA_onPrototype(aRequest) { + return { from: this.actorID, + prototype: this.threadActor.createValueGrip(this.obj.proto) }; + }, + + /** + * Handle a protocol request to provide the property descriptor of the + * object's specified property. + * + * @param aRequest object + * The protocol request object. + */ + onProperty: function OA_onProperty(aRequest) { + if (!aRequest.name) { + return { error: "missingParameter", + message: "no property name was specified" }; + } + + return { from: this.actorID, + descriptor: this._propertyDescriptor(aRequest.name) }; + }, + + /** + * A helper method that creates a property descriptor for the provided object, + * properly formatted for sending in a protocol response. + * + * @param string aName + * The property that the descriptor is generated for. + */ + _propertyDescriptor: function OA_propertyDescriptor(aName) { + let desc; + try { + desc = this.obj.getOwnPropertyDescriptor(aName); + } catch (e) { + // Calling getOwnPropertyDescriptor on wrapped native prototypes is not + // allowed (bug 560072). Inform the user with a bogus, but hopefully + // explanatory, descriptor. + return { + configurable: false, + writable: false, + enumerable: false, + value: e.name + }; + } + + let retval = { + configurable: desc.configurable, + enumerable: desc.enumerable + }; + + if ("value" in desc) { + retval.writable = desc.writable; + retval.value = this.threadActor.createValueGrip(desc.value); + } else { + if ("get" in desc) { + retval.get = this.threadActor.createValueGrip(desc.get); + } + if ("set" in desc) { + retval.set = this.threadActor.createValueGrip(desc.set); + } + } + return retval; + }, + + /** + * Handle a protocol request to provide the source code of a function. + * + * @param aRequest object + * The protocol request object. + */ + onDecompile: function OA_onDecompile(aRequest) { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "decompile request is only valid for object grips " + + "with a 'Function' class." }; + } + + return { from: this.actorID, + decompiledCode: this.obj.decompile(!!aRequest.pretty) }; + }, + + /** + * Handle a protocol request to provide the parameters of a function. + * + * @param aRequest object + * The protocol request object. + */ + onParameterNames: function OA_onParameterNames(aRequest) { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "'parameterNames' request is only valid for object " + + "grips with a 'Function' class." }; + } + + return { parameterNames: this.obj.parameterNames }; + }, + + /** + * Handle a protocol request to release a thread-lifetime grip. + * + * @param aRequest object + * The protocol request object. + */ + onRelease: function OA_onRelease(aRequest) { + this.release(); + return {}; + }, +}; + +ObjectActor.prototype.requestTypes = { + "parameterNames": ObjectActor.prototype.onParameterNames, + "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, + "prototype": ObjectActor.prototype.onPrototype, + "property": ObjectActor.prototype.onProperty, + "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, + "decompile": ObjectActor.prototype.onDecompile, + "release": ObjectActor.prototype.onRelease, +}; + + +/** + * Creates a pause-scoped actor for the specified object. + * @see ObjectActor + */ +function PauseScopedObjectActor() +{ + ObjectActor.apply(this, arguments); +} + +PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); + +update(PauseScopedObjectActor.prototype, ObjectActor.prototype); + +update(PauseScopedObjectActor.prototype, { + constructor: PauseScopedObjectActor, + + onOwnPropertyNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), + + onPrototypeAndProperties: + PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), + + onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), + onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), + onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), + + onParameterNames: + PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), + + /** + * Handle a protocol request to provide the lexical scope of a function. + * + * @param aRequest object + * The protocol request object. + */ + onScope: PauseScopedActor.withPaused(function OA_onScope(aRequest) { + if (this.obj.class !== "Function") { + return { error: "objectNotFunction", + message: "scope request is only valid for object grips with a" + + " 'Function' class." }; + } + + let envActor = this.threadActor.createEnvironmentActor(this.obj.environment, + this.registeredPool); + if (!envActor) { + return { error: "notDebuggee", + message: "cannot access the environment of this function." }; + } + + return { from: this.actorID, scope: envActor.form() }; + }), + + /** + * Handle a protocol request to promote a pause-lifetime grip to a + * thread-lifetime grip. + * + * @param aRequest object + * The protocol request object. + */ + onThreadGrip: PauseScopedActor.withPaused(function OA_onThreadGrip(aRequest) { + this.threadActor.threadObjectGrip(this); + return {}; + }), + + /** + * Handle a protocol request to release a thread-lifetime grip. + * + * @param aRequest object + * The protocol request object. + */ + onRelease: PauseScopedActor.withPaused(function OA_onRelease(aRequest) { + if (this.registeredPool !== this.threadActor.threadLifetimePool) { + return { error: "notReleasable", + message: "Only thread-lifetime actors can be released." }; + } + + this.release(); + return {}; + }), +}); + +update(PauseScopedObjectActor.prototype.requestTypes, { + "scope": PauseScopedObjectActor.prototype.onScope, + "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, +}); + + +/** + * Creates an actor for the specied "very long" string. "Very long" is specified + * at the server's discretion. + * + * @param aString String + * The string. + */ +function LongStringActor(aString) +{ + this.string = aString; + this.stringLength = aString.length; +} + +LongStringActor.prototype = { + + actorPrefix: "longString", + + disconnect: function LSA_disconnect() { + // Because longStringActors is not a weak map, we won't automatically leave + // it so we need to manually leave on disconnect so that we don't leak + // memory. + if (this.registeredPool && this.registeredPool.longStringActors) { + delete this.registeredPool.longStringActors[this.actorID]; + } + }, + + /** + * Returns a grip for this actor for returning in a protocol message. + */ + grip: function LSA_grip() { + return { + "type": "longString", + "initial": this.string.substring( + 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), + "length": this.stringLength, + "actor": this.actorID + }; + }, + + /** + * Handle a request to extract part of this actor's string. + * + * @param aRequest object + * The protocol request object. + */ + onSubstring: function LSA_onSubString(aRequest) { + return { + "from": this.actorID, + "substring": this.string.substring(aRequest.start, aRequest.end) + }; + }, + + /** + * Handle a request to release this LongStringActor instance. + */ + onRelease: function LSA_onRelease() { + // TODO: also check if registeredPool === threadActor.threadLifetimePool + // when the web console moves aray from manually releasing pause-scoped + // actors. + if (this.registeredPool.longStringActors) { + delete this.registeredPool.longStringActors[this.actorID]; + } + this.registeredPool.removeActor(this); + return {}; + }, +}; + +LongStringActor.prototype.requestTypes = { + "substring": LongStringActor.prototype.onSubstring, + "release": LongStringActor.prototype.onRelease +}; + + +/** + * Creates an actor for the specified stack frame. + * + * @param aFrame Debugger.Frame + * The debuggee frame. + * @param aThreadActor ThreadActor + * The parent thread actor for this frame. + */ +function FrameActor(aFrame, aThreadActor) +{ + this.frame = aFrame; + this.threadActor = aThreadActor; +} + +FrameActor.prototype = { + actorPrefix: "frame", + + /** + * A pool that contains frame-lifetime objects, like the environment. + */ + _frameLifetimePool: null, + get frameLifetimePool() { + if (!this._frameLifetimePool) { + this._frameLifetimePool = new ActorPool(this.conn); + this.conn.addActorPool(this._frameLifetimePool); + } + return this._frameLifetimePool; + }, + + /** + * Finalization handler that is called when the actor is being evicted from + * the pool. + */ + disconnect: function FA_disconnect() { + this.conn.removeActorPool(this._frameLifetimePool); + this._frameLifetimePool = null; + }, + + /** + * Returns a frame form for use in a protocol message. + */ + form: function FA_form() { + let form = { actor: this.actorID, + type: this.frame.type }; + if (this.frame.type === "call") { + form.callee = this.threadActor.createValueGrip(this.frame.callee); + } + + if (this.frame.environment) { + let envActor = this.threadActor + .createEnvironmentActor(this.frame.environment, + this.frameLifetimePool); + form.environment = envActor.form(); + } + form.this = this.threadActor.createValueGrip(this.frame.this); + form.arguments = this._args(); + if (this.frame.script) { + form.where = { url: this.frame.script.url, + line: this.frame.script.getOffsetLine(this.frame.offset) }; + form.isBlackBoxed = this.threadActor.sources.isBlackBoxed(this.frame.script.url) + } + + if (!this.frame.older) { + form.oldest = true; + } + + return form; + }, + + _args: function FA__args() { + if (!this.frame.arguments) { + return []; + } + + return [this.threadActor.createValueGrip(arg) + for each (arg in this.frame.arguments)]; + }, + + /** + * Handle a protocol request to pop this frame from the stack. + * + * @param aRequest object + * The protocol request object. + */ + onPop: function FA_onPop(aRequest) { + // TODO: remove this when Debugger.Frame.prototype.pop is implemented + if (typeof this.frame.pop != "function") { + return { error: "notImplemented", + message: "Popping frames is not yet implemented." }; + } + + while (this.frame != this.threadActor.dbg.getNewestFrame()) { + this.threadActor.dbg.getNewestFrame().pop(); + } + this.frame.pop(aRequest.completionValue); + + // TODO: return the watches property when frame pop watch actors are + // implemented. + return { from: this.actorID }; + } +}; + +FrameActor.prototype.requestTypes = { + "pop": FrameActor.prototype.onPop, +}; + + +/** + * Creates a BreakpointActor. BreakpointActors exist for the lifetime of their + * containing thread and are responsible for deleting breakpoints, handling + * breakpoint hits and associating breakpoints with scripts. + * + * @param ThreadActor aThreadActor + * The parent thread actor that contains this breakpoint. + * @param object aLocation + * The location of the breakpoint as specified in the protocol. + */ +function BreakpointActor(aThreadActor, aLocation) +{ + this.scripts = []; + this.threadActor = aThreadActor; + this.location = aLocation; +} + +BreakpointActor.prototype = { + actorPrefix: "breakpoint", + + /** + * Called when this same breakpoint is added to another Debugger.Script + * instance, in the case of a page reload. + * + * @param aScript Debugger.Script + * The new source script on which the breakpoint has been set. + * @param ThreadActor aThreadActor + * The parent thread actor that contains this breakpoint. + */ + addScript: function BA_addScript(aScript, aThreadActor) { + this.threadActor = aThreadActor; + this.scripts.push(aScript); + }, + + /** + * Remove the breakpoints from associated scripts and clear the script cache. + */ + removeScripts: function () { + for (let script of this.scripts) { + script.clearBreakpoint(this); + } + this.scripts = []; + }, + + /** + * A function that the engine calls when a breakpoint has been hit. + * + * @param aFrame Debugger.Frame + * The stack frame that contained the breakpoint. + */ + hit: function BA_hit(aFrame) { + if (this.threadActor.sources.isBlackBoxed(this.location.url)) { + return undefined; + } + + // TODO: add the rest of the breakpoints on that line (bug 676602). + let reason = { type: "breakpoint", actors: [ this.actorID ] }; + return this.threadActor._pauseAndRespond(aFrame, reason, (aPacket) => { + log("pause callback ..."); + + let { url, line } = aPacket.frame.where; + return this.threadActor.sources.getOriginalLocation(url, line) + .then(function (aOrigPosition) { + aPacket.frame.where = aOrigPosition; + return aPacket; + }); + }); + }, + + /** + * Handle a protocol request to remove this breakpoint. + * + * @param aRequest object + * The protocol request object. + */ + onDelete: function BA_onDelete(aRequest) { + // Remove from the breakpoint store. + let scriptBreakpoints = this.threadActor._breakpointStore[this.location.url]; + delete scriptBreakpoints[this.location.line]; + this.threadActor._hooks.removeFromParentPool(this); + // Remove the actual breakpoint from the associated scripts. + this.removeScripts(); + + return { from: this.actorID }; + } +}; + +BreakpointActor.prototype.requestTypes = { + "delete": BreakpointActor.prototype.onDelete +}; + + +/** + * Creates an EnvironmentActor. EnvironmentActors are responsible for listing + * the bindings introduced by a lexical environment and assigning new values to + * those identifier bindings. + * + * @param Debugger.Environment aEnvironment + * The lexical environment that will be used to create the actor. + * @param ThreadActor aThreadActor + * The parent thread actor that contains this environment. + */ +function EnvironmentActor(aEnvironment, aThreadActor) +{ + this.obj = aEnvironment; + this.threadActor = aThreadActor; +} + +EnvironmentActor.prototype = { + actorPrefix: "environment", + + /** + * Return an environment form for use in a protocol message. + */ + form: function EA_form() { + let form = { actor: this.actorID }; + + // What is this environment's type? + if (this.obj.type == "declarative") { + form.type = this.obj.callee ? "function" : "block"; + } else { + form.type = this.obj.type; + } + + // Does this environment have a parent? + if (this.obj.parent) { + form.parent = (this.threadActor + .createEnvironmentActor(this.obj.parent, + this.registeredPool) + .form()); + } + + // Does this environment reflect the properties of an object as variables? + if (this.obj.type == "object" || this.obj.type == "with") { + form.object = this.threadActor.createValueGrip(this.obj.object); + } + + // Is this the environment created for a function call? + if (this.obj.callee) { + form.function = this.threadActor.createValueGrip(this.obj.callee); + } + + // Shall we list this environment's bindings? + if (this.obj.type == "declarative") { + form.bindings = this._bindings(); + } + + return form; + }, + + /** + * Return the identifier bindings object as required by the remote protocol + * specification. + */ + _bindings: function EA_bindings() { + let bindings = { arguments: [], variables: {} }; + + // TODO: this part should be removed in favor of the commented-out part + // below when getVariableDescriptor lands (bug 725815). + if (typeof this.obj.getVariable != "function") { + //if (typeof this.obj.getVariableDescriptor != "function") { + return bindings; + } + + let parameterNames; + if (this.obj.callee) { + parameterNames = this.obj.callee.parameterNames; + } + for each (let name in parameterNames) { + let arg = {}; + // TODO: this part should be removed in favor of the commented-out part + // below when getVariableDescriptor lands (bug 725815). + let desc = { + value: this.obj.getVariable(name), + configurable: false, + writable: true, + enumerable: true + }; + + // let desc = this.obj.getVariableDescriptor(name); + let descForm = { + enumerable: true, + configurable: desc.configurable + }; + if ("value" in desc) { + descForm.value = this.threadActor.createValueGrip(desc.value); + descForm.writable = desc.writable; + } else { + descForm.get = this.threadActor.createValueGrip(desc.get); + descForm.set = this.threadActor.createValueGrip(desc.set); + } + arg[name] = descForm; + bindings.arguments.push(arg); + } + + for each (let name in this.obj.names()) { + if (bindings.arguments.some(function exists(element) { + return !!element[name]; + })) { + continue; + } + + // TODO: this part should be removed in favor of the commented-out part + // below when getVariableDescriptor lands. + let desc = { + configurable: false, + writable: true, + enumerable: true + }; + try { + desc.value = this.obj.getVariable(name); + } catch (e) { + // Avoid "Debugger scope is not live" errors for |arguments|, introduced + // in bug 746601. + if (name != "arguments") { + throw e; + } + } + //let desc = this.obj.getVariableDescriptor(name); + let descForm = { + enumerable: true, + configurable: desc.configurable + }; + if ("value" in desc) { + descForm.value = this.threadActor.createValueGrip(desc.value); + descForm.writable = desc.writable; + } else { + descForm.get = this.threadActor.createValueGrip(desc.get); + descForm.set = this.threadActor.createValueGrip(desc.set); + } + bindings.variables[name] = descForm; + } + + return bindings; + }, + + /** + * Handle a protocol request to change the value of a variable bound in this + * lexical environment. + * + * @param aRequest object + * The protocol request object. + */ + onAssign: function EA_onAssign(aRequest) { + // TODO: enable the commented-out part when getVariableDescriptor lands + // (bug 725815). + /*let desc = this.obj.getVariableDescriptor(aRequest.name); + + if (!desc.writable) { + return { error: "immutableBinding", + message: "Changing the value of an immutable binding is not " + + "allowed" }; + }*/ + + try { + this.obj.setVariable(aRequest.name, aRequest.value); + } catch (e) { + if (e instanceof Debugger.DebuggeeWouldRun) { + return { error: "threadWouldRun", + cause: e.cause ? e.cause : "setter", + message: "Assigning a value would cause the debuggee to run" }; + } + // This should never happen, so let it complain loudly if it does. + throw e; + } + return { from: this.actorID }; + }, + + /** + * Handle a protocol request to fully enumerate the bindings introduced by the + * lexical environment. + * + * @param aRequest object + * The protocol request object. + */ + onBindings: function EA_onBindings(aRequest) { + return { from: this.actorID, + bindings: this._bindings() }; + } +}; + +EnvironmentActor.prototype.requestTypes = { + "assign": EnvironmentActor.prototype.onAssign, + "bindings": EnvironmentActor.prototype.onBindings +}; + +/** + * Override the toString method in order to get more meaningful script output + * for debugging the debugger. + */ +Debugger.Script.prototype.toString = function() { + let output = ""; + if (this.url) { + output += this.url; + } + if (typeof this.startLine != "undefined") { + output += ":" + this.startLine; + if (this.lineCount && this.lineCount > 1) { + output += "-" + (this.startLine + this.lineCount - 1); + } + } + if (this.strictMode) { + output += ":strict"; + } + return output; +}; + +/** + * Helper property for quickly getting to the line number a stack frame is + * currently paused at. + */ +Object.defineProperty(Debugger.Frame.prototype, "line", { + configurable: true, + get: function() { + if (this.script) { + return this.script.getOffsetLine(this.offset); + } else { + return null; + } + } +}); + + +/** + * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a + * thin wrapper over ThreadActor, slightly changing some of its behavior. + * + * @param aConnection object + * The DebuggerServerConnection with which this ChromeDebuggerActor + * is associated. (Currently unused, but required to make this + * constructor usable with addGlobalActor.) + * + * @param aHooks object + * An object with preNest and postNest methods for calling when entering + * and exiting a nested event loop and also addToParentPool and + * removeFromParentPool methods for handling the lifetime of actors that + * will outlive the thread, like breakpoints. + */ +function ChromeDebuggerActor(aConnection, aHooks) +{ + ThreadActor.call(this, aHooks); +} + +ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype); + +update(ChromeDebuggerActor.prototype, { + constructor: ChromeDebuggerActor, + + // A constant prefix that will be used to form the actor ID by the server. + actorPrefix: "chromeDebugger", + + /** + * Override the eligibility check for scripts and sources to make sure every + * script and source with a URL is stored when debugging chrome. + */ + _allowSource: function(aSourceURL) !!aSourceURL, + + /** + * An object that will be used by ThreadActors to tailor their behavior + * depending on the debugging context being required (chrome or content). + * The methods that this object provides must be bound to the ThreadActor + * before use. + */ + globalManager: { + findGlobals: function CDA_findGlobals() { + // Add every global known to the debugger as debuggee. + this.dbg.addAllGlobalsAsDebuggees(); + }, + + /** + * A function that the engine calls when a new global object has been + * created. + * + * @param aGlobal Debugger.Object + * The new global object that was created. + */ + onNewGlobal: function CDA_onNewGlobal(aGlobal) { + this.addDebuggee(aGlobal); + // Notify the client. + this.conn.send({ + from: this.actorID, + type: "newGlobal", + // TODO: after bug 801084 lands see if we need to JSONify this. + hostAnnotations: aGlobal.hostAnnotations + }); + } + } +}); + + +/** + * Manages the sources for a thread. Handles source maps, locations in the + * sources, etc for ThreadActors. + */ +function ThreadSources(aThreadActor, aUseSourceMaps, aAllowPredicate, + aOnNewSource) { + this._thread = aThreadActor; + this._useSourceMaps = aUseSourceMaps; + this._allow = aAllowPredicate; + this._onNewSource = aOnNewSource; + + // source map URL --> promise of SourceMapConsumer + this._sourceMaps = Object.create(null); + // generated source url --> promise of SourceMapConsumer + this._sourceMapsByGeneratedSource = Object.create(null); + // original source url --> promise of SourceMapConsumer + this._sourceMapsByOriginalSource = Object.create(null); + // source url --> SourceActor + this._sourceActors = Object.create(null); + // original url --> generated url + this._generatedUrlsByOriginalUrl = Object.create(null); +} + +/** + * Must be a class property because it needs to persist across reloads, same as + * the breakpoint store. + */ +ThreadSources._blackBoxedSources = new Set(); + +ThreadSources.prototype = { + /** + * Return the source actor representing |aURL|, creating one if none + * exists already. Returns null if |aURL| is not allowed by the 'allow' + * predicate. + * + * Right now this takes a URL, but in the future it should + * take a Debugger.Source. See bug 637572. + * + * @param String aURL + * The source URL. + * @param optional SourceMapConsumer aSourceMap + * The source map that introduced this source, if any. + * @returns a SourceActor representing the source at aURL or null. + */ + source: function TS_source(aURL, aSourceMap=null) { + if (!this._allow(aURL)) { + return null; + } + + if (aURL in this._sourceActors) { + return this._sourceActors[aURL]; + } + + let actor = new SourceActor(aURL, this._thread, aSourceMap); + this._thread.threadLifetimePool.addActor(actor); + this._sourceActors[aURL] = actor; + try { + this._onNewSource(actor); + } catch (e) { + reportError(e); + } + return actor; + }, + + /** + * Return a promise of an array of source actors representing all the + * sources of |aScript|. + * + * If source map handling is enabled and |aScript| has a source map, then + * use it to find all of |aScript|'s *original* sources; return a promise + * of an array of source actors for those. + */ + sourcesForScript: function TS_sourcesForScript(aScript) { + if (!this._useSourceMaps || !aScript.sourceMapURL) { + return resolve([this.source(aScript.url)].filter(isNotNull)); + } + + return this.sourceMap(aScript) + .then((aSourceMap) => { + return [ + this.source(s, aSourceMap) for (s of aSourceMap.sources) + ]; + }) + .then(null, (e) => { + reportError(e); + delete this._sourceMaps[this._normalize(aScript.sourceMapURL, aScript.url)]; + delete this._sourceMapsByGeneratedSource[aScript.url]; + return [this.source(aScript.url)]; + }) + .then(function (aSources) { + return aSources.filter(isNotNull); + }); + }, + + /** + * Return a promise of a SourceMapConsumer for the source map for + * |aScript|; if we already have such a promise extant, return that. + * |aScript| must have a non-null sourceMapURL. + */ + sourceMap: function TS_sourceMap(aScript) { + if (aScript.url in this._sourceMapsByGeneratedSource) { + return this._sourceMapsByGeneratedSource[aScript.url]; + } + dbg_assert(aScript.sourceMapURL); + let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url); + let map = this._fetchSourceMap(sourceMapURL) + .then((aSourceMap) => { + for (let s of aSourceMap.sources) { + this._generatedUrlsByOriginalUrl[s] = aScript.url; + this._sourceMapsByOriginalSource[s] = resolve(aSourceMap); + } + return aSourceMap; + }); + this._sourceMapsByGeneratedSource[aScript.url] = map; + return map; + }, + + /** + * Return a promise of a SourceMapConsumer for the source map located at + * |aAbsSourceMapURL|, which must be absolute. If there is already such a + * promise extant, return it. + */ + _fetchSourceMap: function TS__fetchSourceMap(aAbsSourceMapURL) { + if (aAbsSourceMapURL in this._sourceMaps) { + return this._sourceMaps[aAbsSourceMapURL]; + } else { + let promise = fetch(aAbsSourceMapURL).then((rawSourceMap) => { + let map = new SourceMapConsumer(rawSourceMap); + let base = aAbsSourceMapURL.replace(/\/[^\/]+$/, '/'); + if (base.indexOf("data:") !== 0) { + map.sourceRoot = map.sourceRoot + ? this._normalize(map.sourceRoot, base) + : base; + } + return map; + }); + this._sourceMaps[aAbsSourceMapURL] = promise; + return promise; + } + }, + + /** + * Returns a promise of the location in the original source if the source is + * source mapped, otherwise a promise of the same location. + * + * TODO bug 637572: take/return a column + */ + getOriginalLocation: function TS_getOriginalLocation(aSourceUrl, aLine) { + if (aSourceUrl in this._sourceMapsByGeneratedSource) { + return this._sourceMapsByGeneratedSource[aSourceUrl] + .then(function (aSourceMap) { + let { source, line } = aSourceMap.originalPositionFor({ + source: aSourceUrl, + line: aLine, + column: Infinity + }); + return { + url: source, + line: line + }; + }); + } + + // No source map + return resolve({ + url: aSourceUrl, + line: aLine + }); + }, + + /** + * Returns a promise of the location in the generated source corresponding to + * the original source and line given. + * + * When we pass a script S representing generated code to |sourceMap|, + * above, that returns a promise P. The process of resolving P populates + * the tables this function uses; thus, it won't know that S's original + * source URLs map to S until P is resolved. + * + * TODO bug 637572: take/return a column + */ + getGeneratedLocation: function TS_getGeneratedLocation(aSourceUrl, aLine) { + if (aSourceUrl in this._sourceMapsByOriginalSource) { + return this._sourceMapsByOriginalSource[aSourceUrl] + .then((aSourceMap) => { + let { line } = aSourceMap.generatedPositionFor({ + source: aSourceUrl, + line: aLine, + column: Infinity + }); + return { + url: this._generatedUrlsByOriginalUrl[aSourceUrl], + line: line + }; + }); + } + + // No source map + return resolve({ + url: aSourceUrl, + line: aLine + }); + }, + + /** + * Returns true if URL for the given source is black boxed. + * + * @param aURL String + * The URL of the source which we are checking whether it is black + * boxed or not. + */ + isBlackBoxed: function TS_isBlackBoxed(aURL) { + return ThreadSources._blackBoxedSources.has(aURL); + }, + + /** + * Add the given source URL to the set of sources that are black boxed. If the + * thread is currently paused and we are black boxing the yougest frame's + * source, this will force a step. + * + * @param aURL String + * The URL of the source which we are black boxing. + */ + blackBox: function TS_blackBox(aURL) { + ThreadSources._blackBoxedSources.add(aURL); + }, + + /** + * Remove the given source URL to the set of sources that are black boxed. + * + * @param aURL String + * The URL of the source which we are no longer black boxing. + */ + unblackBox: function TS_unblackBox(aURL) { + ThreadSources._blackBoxedSources.delete(aURL); + }, + + /** + * Normalize multiple relative paths towards the base paths on the right. + */ + _normalize: function TS__normalize(...aURLs) { + dbg_assert(aURLs.length > 1); + let base = Services.io.newURI(aURLs.pop(), null, null); + let url; + while ((url = aURLs.pop())) { + base = Services.io.newURI(url, null, base); + } + return base.spec; + }, + + iter: function TS_iter() { + for (let url in this._sourceActors) { + yield this._sourceActors[url]; + } + } +}; + +// Utility functions. + +/** + * Utility function for updating an object with the properties of another + * object. + * + * @param aTarget Object + * The object being updated. + * @param aNewAttrs Object + * The new attributes being set on the target. + */ +function update(aTarget, aNewAttrs) { + for (let key in aNewAttrs) { + let desc = Object.getOwnPropertyDescriptor(aNewAttrs, key); + + if (desc) { + Object.defineProperty(aTarget, key, desc); + } + } +} + +/** + * Returns true if its argument is not null. + */ +function isNotNull(aThing) { + return aThing !== null; +} + +/** + * Performs a request to load the desired URL and returns a promise. + * + * @param aURL String + * The URL we will request. + * @returns Promise + * A promise of the document at that URL, as a string. + * + * XXX: It may be better to use nsITraceableChannel to get to the sources + * without relying on caching when we can (not for eval, etc.): + * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/ + */ +function fetch(aURL, aOptions={ loadFromCache: true }) { + let deferred = defer(); + let scheme; + let url = aURL.split(" -> ").pop(); + let charset; + let filePath = url; + // try { + // scheme = Services.io.extractScheme(url); + // } catch (e) { + // In the xpcshell tests, the script url is the absolute path of the test + // file, which will make a malformed URI error be thrown. Add the file + // scheme prefix ourselves. + url = "file://" + url; + // scheme = Services.io.extractScheme(url); + // } + scheme = "file"; + + switch (scheme) { + case "file": + case "chrome": + case "resource": + try { + // NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) { + // if (!Components.isSuccessCode(aStatus)) { + // deferred.reject("Request failed: " + url); + // return; + // } + let cc = globalDebuggee.cc; + let fileUtils = cc.FileUtils.getInstance(); + let source = fileUtils.getStringFromFile(filePath);//NetUtil.readInputStreamToString(aStream, aStream.available()); + if (!source) + { + deferred.reject("Request failed: " + url); + } + else + { + deferred.resolve(source); + } + // aStream.close(); + // }); + } catch (ex) { + deferred.reject("Request failed: " + url); + } + break; + + default: + let channel; + try { + channel = Services.io.newChannel(url, null, null); + } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { + // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but + // newChannel won't be able to handle it. + url = "file:///" + url; + channel = Services.io.newChannel(url, null, null); + } + let chunks = []; + let streamListener = { + onStartRequest: function(aRequest, aContext, aStatusCode) { + if (!Components.isSuccessCode(aStatusCode)) { + deferred.reject("Request failed: " + url); + } + }, + onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { + chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); + }, + onStopRequest: function(aRequest, aContext, aStatusCode) { + if (!Components.isSuccessCode(aStatusCode)) { + deferred.reject("Request failed: " + url); + return; + } + + charset = channel.contentCharset; + deferred.resolve(chunks.join("")); + } + }; + + channel.loadFlags = aOptions.loadFromCache + ? channel.LOAD_FROM_CACHE + : channel.LOAD_BYPASS_CACHE; + channel.asyncOpen(streamListener, null); + break; + } + + return deferred.promise.then(function (source) { + return convertToUnicode(source, charset); + }); +} + +/** + * Convert a given string, encoded in a given character set, to unicode. + * + * @param string aString + * A string. + * @param string aCharset + * A character set. + */ +function convertToUnicode(aString, aCharset=null) { + // Decoding primitives. + // let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + // .createInstance(Ci.nsIScriptableUnicodeConverter); + // try { + // converter.charset = aCharset || "UTF-8"; + // return converter.ConvertToUnicode(aString); + // } catch(e) { + return aString; + // } +} + +/** + * Report the given error in the error console and to stdout. + */ +function reportError(aError) { + // Cu.reportError(aError); + dumpn(aError.message + ":\n" + aError.stack); +} diff --git a/cocos/scripting/javascript/bindings/js/debugger/core/promise.js b/cocos/scripting/javascript/bindings/js/debugger/core/promise.js new file mode 100644 index 0000000000..a695f5a3ad --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/core/promise.js @@ -0,0 +1,294 @@ +/* vim:set ts=2 sw=2 sts=2 expandtab */ +/*jshint undef: true es5: true node: true browser: true devel: true + forin: true latedef: false */ +/*global define: true, Cu: true, __URI__: true */ +//;(function(id, factory) { // Module boilerplate :( +// if (typeof(define) === 'function') { // RequireJS +// define(factory); +// } else if (typeof(require) === 'function') { // CommonJS +// factory.call(this, require, exports, module); +// } else if (String(this).indexOf('BackstagePass') >= 0) { // JSM +// this[factory.name] = {}; +// try { +// this.console = this['Components'].utils +// .import('resource://gre/modules/devtools/Console.jsm', {}).console; +// } +// catch (ex) { +// // Avoid failures on different toolkit configurations. +// } +// factory(function require(uri) { +// var imports = {}; +// this['Components'].utils.import(uri, imports); +// return imports; +// }, this[factory.name], { uri: __URI__, id: id }); +// this.EXPORTED_SYMBOLS = [factory.name]; +// } else { // Browser or alike +// var globals = this; +// factory(function require(id) { +// return globals[id]; +// }, (globals[id] = {}), { uri: document.location.href + '#' + id, id: id }); +// } +//}).call(this, 'promise/core', function Promise(require, exports, module) { + +'use strict'; + +var exports = exports || {}; + +//module.metadata = { +// "stability": "unstable" +//}; + +/** + * Internal utility: Wraps given `value` into simplified promise, successfully + * fulfilled to a given `value`. Note the result is not a complete promise + * implementation, as its method `then` does not returns anything. + */ +function fulfilled(value) { + return { then: function then(fulfill) { fulfill(value); } }; +} + +/** + * Internal utility: Wraps given input into simplified promise, pre-rejected + * with a given `reason`. Note the result is not a complete promise + * implementation, as its method `then` does not returns anything. + */ +function rejected(reason) { + return { then: function then(fulfill, reject) { reject(reason); } }; +} + +/** + * Internal utility: Returns `true` if given `value` is a promise. Value is + * assumed to be a promise if it implements method `then`. + */ +function isPromise(value) { + return value && typeof(value.then) === 'function'; +} + +/** + * Creates deferred object containing fresh promise & methods to either resolve + * or reject it. The result is an object with the following properties: + * - `promise` Eventual value representation implementing CommonJS [Promises/A] + * (http://wiki.commonjs.org/wiki/Promises/A) API. + * - `resolve` Single shot function that resolves enclosed `promise` with a + * given `value`. + * - `reject` Single shot function that rejects enclosed `promise` with a given + * `reason`. + * + * An optional `prototype` argument is used as a prototype of the returned + * `promise` allowing one to implement additional API. If prototype is not + * passed then it falls back to `Object.prototype`. + * + * ## Example + * + * function fetchURI(uri, type) { + * var deferred = defer(); + * var request = new XMLHttpRequest(); + * request.open("GET", uri, true); + * request.responseType = type; + * request.onload = function onload() { + * deferred.resolve(request.response); + * } + * request.onerror = function(event) { + * deferred.reject(event); + * } + * request.send(); + * + * return deferred.promise; + * } + */ +function defer(prototype) { + // Define FIFO queue of observer pairs. Once promise is resolved & all queued + // observers are forwarded to `result` and variable is set to `null`. + var observers = []; + + // Promise `result`, which will be assigned a resolution value once promise + // is resolved. Note that result will always be assigned promise (or alike) + // object to take care of propagation through promise chains. If result is + // `null` promise is not resolved yet. + var result = null; + + prototype = (prototype || prototype === null) ? prototype : Object.prototype; + + // Create an object implementing promise API. + var promise = Object.create(prototype, { + then: { value: function then(onFulfill, onError) { + var deferred = defer(prototype); + + function resolve(value) { + // If `onFulfill` handler is provided resolve `deferred.promise` with + // result of invoking it with a resolution value. If handler is not + // provided propagate value through. + try { + deferred.resolve(onFulfill ? onFulfill(value) : value); + } + // `onFulfill` may throw exception in which case resulting promise + // is rejected with thrown exception. + catch(error) { + if (exports._reportErrors && typeof(console) === 'object') + console.error(error); + // Note: Following is equivalent of `deferred.reject(error)`, + // we use this shortcut to reduce a stack. + deferred.resolve(rejected(error)); + } + } + + function reject(reason) { + try { + if (onError) deferred.resolve(onError(reason)); + else deferred.resolve(rejected(reason)); + } + catch(error) { + if (exports._reportErrors && typeof(console) === 'object') + console.error(error); + deferred.resolve(rejected(error)); + } + } + + // If enclosed promise (`this.promise`) observers queue is still alive + // enqueue a new observer pair into it. Note that this does not + // necessary means that promise is pending, it may already be resolved, + // but we still have to queue observers to guarantee an order of + // propagation. + if (observers) { + observers.push({ resolve: resolve, reject: reject }); + } + // Otherwise just forward observer pair right to a `result` promise. + else { + result.then(resolve, reject); + } + + return deferred.promise; + }} + }) + + var deferred = { + promise: promise, + /** + * Resolves associated `promise` to a given `value`, unless it's already + * resolved or rejected. Note that resolved promise is not necessary a + * successfully fulfilled. Promise may be resolved with a promise `value` + * in which case `value` promise's fulfillment / rejection will propagate + * up to a promise resolved with `value`. + */ + resolve: function resolve(value) { + if (!result) { + // Store resolution `value` in a `result` as a promise, so that all + // the subsequent handlers can be simply forwarded to it. Since + // `result` will be a promise all the value / error propagation will + // be uniformly taken care of. + result = isPromise(value) ? value : fulfilled(value); + + // Forward already registered observers to a `result` promise in the + // order they were registered. Note that we intentionally dequeue + // observer at a time until queue is exhausted. This makes sure that + // handlers registered as side effect of observer forwarding are + // queued instead of being invoked immediately, guaranteeing FIFO + // order. + while (observers.length) { + var observer = observers.shift(); + result.then(observer.resolve, observer.reject); + } + + // Once `observers` queue is exhausted we `null`-ify it, so that + // new handlers are forwarded straight to the `result`. + observers = null; + } + }, + /** + * Rejects associated `promise` with a given `reason`, unless it's already + * resolved / rejected. This is just a (better performing) convenience + * shortcut for `deferred.resolve(reject(reason))`. + */ + reject: function reject(reason) { + // Note that if promise is resolved that does not necessary means that it + // is successfully fulfilled. Resolution value may be a promise in which + // case its result propagates. In other words if promise `a` is resolved + // with promise `b`, `a` is either fulfilled or rejected depending + // on weather `b` is fulfilled or rejected. Here `deferred.promise` is + // resolved with a promise pre-rejected with a given `reason`, there for + // `deferred.promise` is rejected with a given `reason`. This may feel + // little awkward first, but doing it this way greatly simplifies + // propagation through promise chains. + deferred.resolve(rejected(reason)); + } + }; + + return deferred; +} +exports.defer = defer; + +/** + * Returns a promise resolved to a given `value`. Optionally a second + * `prototype` argument may be provided to be used as a prototype for the + * returned promise. + */ +function resolve(value, prototype) { + var deferred = defer(prototype); + deferred.resolve(value); + return deferred.promise; +} +exports.resolve = resolve; + +/** + * Returns a promise rejected with a given `reason`. Optionally a second + * `prototype` argument may be provided to be used as a prototype for the + * returned promise. + */ +function reject(reason, prototype) { + var deferred = defer(prototype); + deferred.reject(reason); + return deferred.promise; +} +exports.reject = reject; + +var promised = (function() { + // Note: Define shortcuts and utility functions here in order to avoid + // slower property accesses and unnecessary closure creations on each + // call of this popular function. + + var call = Function.call; + var concat = Array.prototype.concat; + + // Utility function that does following: + // execute([ f, self, args...]) => f.apply(self, args) + function execute(args) { return call.apply(call, args) } + + // Utility function that takes promise of `a` array and maybe promise `b` + // as arguments and returns promise for `a.concat(b)`. + function promisedConcat(promises, unknown) { + return promises.then(function(values) { + return resolve(unknown).then(function(value) { + return values.concat([ value ]); + }); + }); + } + + return function promised(f, prototype) { + /** + Returns a wrapped `f`, which when called returns a promise that resolves to + `f(...)` passing all the given arguments to it, which by the way may be + promises. Optionally second `prototype` argument may be provided to be used + a prototype for a returned promise. + + ## Example + + var promise = promised(Array)(1, promise(2), promise(3)) + promise.then(console.log) // => [ 1, 2, 3 ] + **/ + + return function promised() { + // create array of [ f, this, args... ] + return concat.apply([ f, this ], arguments). + // reduce it via `promisedConcat` to get promised array of fulfillments + reduce(promisedConcat, resolve([], prototype)). + // finally map that to promise of `f.apply(this, args...)` + then(execute); + }; + } +})(); +exports.promised = promised; +// +var all = promised(Array); +exports.all = all; +// +//}); diff --git a/cocos/scripting/javascript/bindings/js/debugger/main.js b/cocos/scripting/javascript/bindings/js/debugger/main.js new file mode 100644 index 0000000000..f08009ade6 --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/main.js @@ -0,0 +1,928 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +/** + * Toolkit glue for the remote debugging protocol, loaded into the + * debugging global. + */ + +//const Ci = Components.interfaces; +//const Cc = Components.classes; +//const CC = Components.Constructor; +//const Cu = Components.utils; +//const Cr = Components.results; +//const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties"; +// +//Cu.import("resource://gre/modules/Services.jsm"); +//Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +//let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +// +//Cu.import("resource://gre/modules/jsdebugger.jsm"); +//addDebuggerToGlobal(this); +// +//Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); +//const { defer, resolve, reject, all } = Promise; +// +//Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); +// +//loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js"); + +let wantLogging = true; +let debuggerServer = null; + +function dumpn(str) { + if (wantLogging) { + log("DBG-SERVER: " + str + "\n"); + } +} + +function dbg_assert(cond, e) { + if (!cond) { + log("assert >>>> " + cond.toString()); + return e; + } +} + +function XPCInspector() { + this.exitNestedEventLoop = _exitNestedEventLoop; + this.enterNestedEventLoop = _enterNestedEventLoop; + this.eventLoopNestLevel = _getEventLoopNestLevel; +} + +//loadSubScript.call(this, "resource://gre/modules/devtools/server/transport.js"); + +// XPCOM constructors +// const ServerSocket = CC("@mozilla.org/network/server-socket;1", +// "nsIServerSocket", +// "initSpecialConnection"); + +function ServerSocket(aPort, flags, c){ + this.asyncListen = function(server){ + log("asyncListen...."); + debuggerServer = server; + }; +}; + +var gRegisteredModules = Object.create(null); + +/** + * The ModuleAPI object is passed to modules loaded using the + * DebuggerServer.registerModule() API. Modules can use this + * object to register actor factories. + * Factories registered through the module API will be removed + * when the module is unregistered or when the server is + * destroyed. + */ +function ModuleAPI() { + let activeTabActors = new Set(); + let activeGlobalActors = new Set(); + + return { + // See DebuggerServer.addGlobalActor for a description. + addGlobalActor: function(factory, name) { + DebuggerServer.addGlobalActor(factory, name); + activeGlobalActors.add(factory); + }, + // See DebuggerServer.removeGlobalActor for a description. + removeGlobalActor: function(factory) { + DebuggerServer.removeGlobalActor(factory); + activeGlobalActors.delete(factory); + }, + + // See DebuggerServer.addTabActor for a description. + addTabActor: function(factory, name) { + DebuggerServer.addTabActor(factory, name); + activeTabActors.add(factory); + }, + // See DebuggerServer.removeTabActor for a description. + removeTabActor: function(factory) { + DebuggerServer.removeTabActor(factory); + activeTabActors.delete(factory); + }, + + // Destroy the module API object, unregistering any + // factories registered by the module. + destroy: function() { + for (let factory of activeTabActors) { + DebuggerServer.removeTabActor(factory); + } + activeTabActors = null; + for (let factory of activeGlobalActors) { + DebuggerServer.removeGlobalActor(factory); + } + activeGlobalActors = null; + } + } +}; + +/*** + * Public API + */ +var DebuggerServer = { + _listener: null, + _initialized: false, + _transportInitialized: false, + xpcInspector: null, + _transport: null, + // Number of currently open TCP connections. + _socketConnections: 0, + // Map of global actor names to actor constructors provided by extensions. + globalActorFactories: {}, + // Map of tab actor names to actor constructors provided by extensions. + tabActorFactories: {}, + + LONG_STRING_LENGTH: 10000, + LONG_STRING_INITIAL_LENGTH: 1000, + LONG_STRING_READ_LENGTH: 1000, + + /** + * A handler function that prompts the user to accept or decline the incoming + * connection. + */ + _allowConnection: null, + + /** + * Prompt the user to accept or decline the incoming connection. This is the + * default implementation that products embedding the debugger server may + * choose to override. + * + * @return true if the connection should be permitted, false otherwise + */ + _defaultAllowConnection: function DS__defaultAllowConnection() { + // let title = L10N.getStr("remoteIncomingPromptTitle"); + // let msg = L10N.getStr("remoteIncomingPromptMessage"); + // let disableButton = L10N.getStr("remoteIncomingPromptDisable"); + // let prompt = Services.prompt; + // let flags = prompt.BUTTON_POS_0 * prompt.BUTTON_TITLE_OK + + // prompt.BUTTON_POS_1 * prompt.BUTTON_TITLE_CANCEL + + // prompt.BUTTON_POS_2 * prompt.BUTTON_TITLE_IS_STRING + + // prompt.BUTTON_POS_1_DEFAULT; + // let result = prompt.confirmEx(null, title, msg, flags, null, null, + // disableButton, null, { value: false }); + // if (result == 0) { + // return true; + // } + // if (result == 2) { + // DebuggerServer.closeListener(true); + // Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false); + // } + // return false; + return true; + }, + + /** + * Initialize the debugger server. + * + * @param function aAllowConnectionCallback + * The embedder-provider callback, that decides whether an incoming + * remote protocol conection should be allowed or refused. + */ + init: function DS_init(aAllowConnectionCallback) { + if (this.initialized) { + return; + } + + this.xpcInspector = new XPCInspector();//Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector); + this.initTransport(aAllowConnectionCallback); + this.addActors("resource://gre/modules/devtools/server/actors/root.js"); + + this._initialized = true; + }, + + /** + * Initialize the debugger server's transport variables. This can be + * in place of init() for cases where the jsdebugger isn't needed. + * + * @param function aAllowConnectionCallback + * The embedder-provider callback, that decides whether an incoming + * remote protocol conection should be allowed or refused. + */ + initTransport: function DS_initTransport(aAllowConnectionCallback) { + if (this._transportInitialized) { + return; + } + + this._connections = {}; + this._nextConnID = 0; + this._transportInitialized = true; + this._allowConnection = aAllowConnectionCallback ? + aAllowConnectionCallback : + this._defaultAllowConnection; + }, + + get initialized() this._initialized, + + /** + * Performs cleanup tasks before shutting down the debugger server, if no + * connections are currently open. Such tasks include clearing any actor + * constructors added at runtime. This method should be called whenever a + * debugger server is no longer useful, to avoid memory leaks. After this + * method returns, the debugger server must be initialized again before use. + */ + destroy: function DS_destroy() { + if (!this._initialized) { + return; + } + + for (let connID of Object.getOwnPropertyNames(this._connections)) { + this._connections[connID].close(); + } + + for (let id of Object.getOwnPropertyNames(gRegisteredModules)) { + let mod = gRegisteredModules[id]; + mod.module.unregister(mod.api); + } + gRegisteredModules = {}; + + this.closeListener(); + this.globalActorFactories = {}; + this.tabActorFactories = {}; + delete this._allowConnection; + this._transportInitialized = false; + this._initialized = false; + dumpn("Debugger server is shut down."); + }, + + /** + * Load a subscript into the debugging global. + * + * @param aURL string A url that will be loaded as a subscript into the + * debugging global. The user must load at least one script + * that implements a createRootActor() function to create the + * server's root actor. + */ + addActors: function DS_addActors(aURL) { + //loadSubScript.call(this, aURL); + }, + + /** + * Register a CommonJS module with the debugger server. + * @param id string + * The ID of a CommonJS module. This module must export + * 'register' and 'unregister' functions. + */ + registerModule: function(id) { + if (id in gRegisteredModules) { + throw new Error("Tried to register a module twice: " + id + "\n"); + } + + let moduleAPI = ModuleAPI(); + + let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + let mod = devtools.require(id); + mod.register(moduleAPI); + gRegisteredModules[id] = { module: mod, api: moduleAPI }; + }, + + /** + * Returns true if a module id has been registered. + */ + isModuleRegistered: function(id) { + return (id in gRegisteredModules); + }, + + /** + * Unregister a previously-loaded CommonJS module from the debugger server. + */ + unregisterModule: function(id) { + let mod = gRegisteredModules[id]; + if (!mod) { + throw new Error("Tried to unregister a module that was not previously registered."); + } + mod.module.unregister(mod.api); + mod.api.destroy(); + delete gRegisteredModules[id]; + }, + + /** + * Install Firefox-specific actors. + */ + addBrowserActors: function DS_addBrowserActors() { + this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js"); + this.addActors("resource://gre/modules/devtools/server/actors/script.js"); + this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger"); + this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js"); + this.addActors("resource://gre/modules/devtools/server/actors/gcli.js"); + if ("nsIProfiler" in Ci) + this.addActors("resource://gre/modules/devtools/server/actors/profiler.js"); + + this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js"); + this.addActors("resource://gre/modules/devtools/server/actors/webapps.js"); + this.registerModule("devtools/server/actors/inspector"); + }, + + /** + * Listens on the given port for remote debugger connections. + * + * @param aPort int + * The port to listen on. + */ + openListener: function DS_openListener(aPort) { + // if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) { + // return false; + // } + this._checkInit(); + + // Return early if the server is already listening. + if (this._listener) { + return true; + } + + // let flags = Ci.nsIServerSocket.KeepWhenOffline; + // A preference setting can force binding on the loopback interface. + // if (Services.prefs.getBoolPref("devtools.debugger.force-local")) { + // flags |= Ci.nsIServerSocket.LoopbackOnly; + // } + + let flags = 0; + + try { + let socket = new ServerSocket(aPort, flags, 4); + socket.asyncListen(this); + this._listener = socket; + } catch (e) { + dumpn("Could not start debugging listener on port " + aPort + ": " + e); + throw "Cr.NS_ERROR_NOT_AVAILABLE"; + } + this._socketConnections++; + + return true; + }, + + /** + * Close a previously-opened TCP listener. + * + * @param aForce boolean [optional] + * If set to true, then the socket will be closed, regardless of the + * number of open connections. + */ + closeListener: function DS_closeListener(aForce) { + if (!this._listener || this._socketConnections == 0) { + return false; + } + + // Only close the listener when the last connection is closed, or if the + // aForce flag is passed. + if (--this._socketConnections == 0 || aForce) { + this._listener.close(); + this._listener = null; + this._socketConnections = 0; + } + + return true; + }, + + /** + * Creates a new connection to the local debugger speaking over a fake + * transport. This connection results in straightforward calls to the onPacket + * handlers of each side. + * + * @returns a client-side DebuggerTransport for communicating with + * the newly-created connection. + */ + connectPipe: function DS_connectPipe() { + this._checkInit(); + + let serverTransport = new LocalDebuggerTransport; + let clientTransport = new LocalDebuggerTransport(serverTransport); + serverTransport.other = clientTransport; + let connection = this._onConnection(serverTransport); + + // I'm putting this here because I trust you. + // + // There are times, when using a local connection, when you're going + // to be tempted to just get direct access to the server. Resist that + // temptation! If you succumb to that temptation, you will make the + // fine developers that work on Fennec and Firefox OS sad. They're + // professionals, they'll try to act like they understand, but deep + // down you'll know that you hurt them. + // + // This reference allows you to give in to that temptation. There are + // times this makes sense: tests, for example, and while porting a + // previously local-only codebase to the remote protocol. + // + // But every time you use this, you will feel the shame of having + // used a property that starts with a '_'. + clientTransport._serverConnection = connection; + + return clientTransport; + }, + + + // nsIServerSocketListener implementation + + onSocketAccepted: + makeInfallible(function DS_onSocketAccepted(aSocket, aTransport) { + if (!this._allowConnection()) { + return; + } + dumpn("New debugging connection on " + aTransport.host + ":" + aTransport.port); + + let input = aTransport.openInputStream(0, 0, 0); + let output = aTransport.openOutputStream(0, 0, 0); + let transport = new DebuggerTransport(input, output); + DebuggerServer._onConnection(transport); + }, "DebuggerServer.onSocketAccepted"), + + onStopListening: function DS_onStopListening(aSocket, status) { + dumpn("onStopListening, status: " + status); + }, + + /** + * Raises an exception if the server has not been properly initialized. + */ + _checkInit: function DS_checkInit() { + if (!this._transportInitialized) { + throw "DebuggerServer has not been initialized."; + } + + if (!this.createRootActor) { + throw "Use DebuggerServer.addActors() to add a root actor implementation."; + } + }, + + /** + * Create a new debugger connection for the given transport. Called + * after connectPipe() or after an incoming socket connection. + */ + _onConnection: function DS_onConnection(aTransport) { + log("DebuggerServer._onConnection...."); + + this._transport = aTransport; + + let connID = "conn" + this._nextConnID++ + '.'; + let conn = new DebuggerServerConnection(connID, aTransport); + this._connections[connID] = conn; + + // Create a root actor for the connection and send the hello packet. + conn.rootActor = this.createRootActor(conn); + conn.addActor(conn.rootActor); + + aTransport.send(conn.rootActor.sayHello()); + aTransport.ready(); + + return conn; + }, + + /** + * Remove the connection from the debugging server. + */ + _connectionClosed: function DS_connectionClosed(aConnection) { + delete this._connections[aConnection.prefix]; + }, + + // DebuggerServer extension API. + + /** + * Registers handlers for new tab-scoped request types defined dynamically. + * This is used for example by add-ons to augment the functionality of the tab + * actor. Note that the name or actorPrefix of the request type is not allowed + * to clash with existing protocol packet properties, like 'title', 'url' or + * 'actor', since that would break the protocol. + * + * @param aFunction function + * The constructor function for this request type. This expects to be + * called as a constructor (i.e. with 'new'), and passed two + * arguments: the DebuggerServerConnection, and the BrowserTabActor + * with which it will be associated. + * + * @param aName string [optional] + * The name of the new request type. If this is not present, the + * actorPrefix property of the constructor prototype is used. + */ + addTabActor: function DS_addTabActor(aFunction, aName) { + let name = aName ? aName : aFunction.prototype.actorPrefix; + if (["title", "url", "actor"].indexOf(name) != -1) { + throw Error(name + " is not allowed"); + } + if (DebuggerServer.tabActorFactories.hasOwnProperty(name)) { + throw Error(name + " already exists"); + } + DebuggerServer.tabActorFactories[name] = aFunction; + }, + + /** + * Unregisters the handler for the specified tab-scoped request type. + * This may be used for example by add-ons when shutting down or upgrading. + * + * @param aFunction function + * The constructor function for this request type. + */ + removeTabActor: function DS_removeTabActor(aFunction) { + for (let name in DebuggerServer.tabActorFactories) { + let handler = DebuggerServer.tabActorFactories[name]; + if (handler.name == aFunction.name) { + delete DebuggerServer.tabActorFactories[name]; + } + } + }, + + /** + * Registers handlers for new browser-scoped request types defined + * dynamically. This is used for example by add-ons to augment the + * functionality of the root actor. Note that the name or actorPrefix of the + * request type is not allowed to clash with existing protocol packet + * properties, like 'from', 'tabs' or 'selected', since that would break the + * protocol. + * + * @param aFunction function + * The constructor function for this request type. This expects to be + * called as a constructor (i.e. with 'new'), and passed two + * arguments: the DebuggerServerConnection, and the BrowserRootActor + * with which it will be associated. + * + * @param aName string [optional] + * The name of the new request type. If this is not present, the + * actorPrefix property of the constructor prototype is used. + */ + addGlobalActor: function DS_addGlobalActor(aFunction, aName) { + let name = aName ? aName : aFunction.prototype.actorPrefix; + if (["from", "tabs", "selected"].indexOf(name) != -1) { + throw Error(name + " is not allowed"); + } + if (DebuggerServer.globalActorFactories.hasOwnProperty(name)) { + throw Error(name + " already exists"); + } + DebuggerServer.globalActorFactories[name] = aFunction; + }, + + /** + * Unregisters the handler for the specified browser-scoped request type. + * This may be used for example by add-ons when shutting down or upgrading. + * + * @param aFunction function + * The constructor function for this request type. + */ + removeGlobalActor: function DS_removeGlobalActor(aFunction) { + for (let name in DebuggerServer.globalActorFactories) { + let handler = DebuggerServer.globalActorFactories[name]; + if (handler.name == aFunction.name) { + delete DebuggerServer.globalActorFactories[name]; + } + } + } +}; + + +/** + * Construct an ActorPool. + * + * ActorPools are actorID -> actor mapping and storage. These are + * used to accumulate and quickly dispose of groups of actors that + * share a lifetime. + */ +function ActorPool(aConnection) +{ + this.conn = aConnection; + this._cleanups = {}; + this._actors = {}; +} + +ActorPool.prototype = { + /** + * Add an actor to the actor pool. If the actor doesn't have an ID, + * allocate one from the connection. + * + * @param aActor object + * The actor implementation. If the object has a + * 'disconnect' property, it will be called when the actor + * pool is cleaned up. + */ + addActor: function AP_addActor(aActor) { + aActor.conn = this.conn; + if (!aActor.actorID) { + let prefix = aActor.actorPrefix; + if (typeof aActor == "function") { + prefix = aActor.prototype.actorPrefix; + } + aActor.actorID = this.conn.allocID(prefix || undefined); + } + + if (aActor.registeredPool) { + aActor.registeredPool.removeActor(aActor); + } + aActor.registeredPool = this; + + this._actors[aActor.actorID] = aActor; + if (aActor.disconnect) { + this._cleanups[aActor.actorID] = aActor; + } + }, + + get: function AP_get(aActorID) { + return this._actors[aActorID]; + }, + + has: function AP_has(aActorID) { + return aActorID in this._actors; + }, + + /** + * Returns true if the pool is empty. + */ + isEmpty: function AP_isEmpty() { + return Object.keys(this._actors).length == 0; + }, + + /** + * Remove an actor from the actor pool. + */ + removeActor: function AP_remove(aActor) { + delete this._actors[aActor.actorID]; + delete this._cleanups[aActor.actorID]; + }, + + /** + * Match the api expected by the protocol library. + */ + unmanage: function(aActor) { + return this.removeActor(aActor); + }, + + /** + * Run all actor cleanups. + */ + cleanup: function AP_cleanup() { + for each (let actor in this._cleanups) { + actor.disconnect(); + } + this._cleanups = {}; + } +} + +/** + * Creates a DebuggerServerConnection. + * + * Represents a connection to this debugging global from a client. + * Manages a set of actors and actor pools, allocates actor ids, and + * handles incoming requests. + * + * @param aPrefix string + * All actor IDs created by this connection should be prefixed + * with aPrefix. + * @param aTransport transport + * Packet transport for the debugging protocol. + */ +function DebuggerServerConnection(aPrefix, aTransport) +{ + this._prefix = aPrefix; + this._transport = aTransport; + this._transport.hooks = this; + this._nextID = 1; + + this._actorPool = new ActorPool(this); + this._extraPools = []; +} + +DebuggerServerConnection.prototype = { + _prefix: null, + get prefix() { return this._prefix }, + + _transport: null, + get transport() { return this._transport }, + + close: function() { + this._transport.close(); + }, + + send: function DSC_send(aPacket) { + this.transport.send(aPacket); + }, + + allocID: function DSC_allocID(aPrefix) { + return this.prefix + (aPrefix || '') + this._nextID++; + }, + + /** + * Add a map of actor IDs to the connection. + */ + addActorPool: function DSC_addActorPool(aActorPool) { + this._extraPools.push(aActorPool); + }, + + /** + * Remove a previously-added pool of actors to the connection. + * + * @param ActorPool aActorPool + * The ActorPool instance you want to remove. + * @param boolean aCleanup + * True if you want to disconnect each actor from the pool, false + * otherwise. + */ + removeActorPool: function DSC_removeActorPool(aActorPool, aCleanup) { + let index = this._extraPools.lastIndexOf(aActorPool); + if (index > -1) { + let pool = this._extraPools.splice(index, 1); + if (aCleanup) { + pool.map(function(p) { p.cleanup(); }); + } + } + }, + + /** + * Add an actor to the default actor pool for this connection. + */ + addActor: function DSC_addActor(aActor) { + this._actorPool.addActor(aActor); + }, + + /** + * Remove an actor to the default actor pool for this connection. + */ + removeActor: function DSC_removeActor(aActor) { + this._actorPool.removeActor(aActor); + }, + + /** + * Match the api expected by the protocol library. + */ + unmanage: function(aActor) { + return this.removeActor(aActor); + }, + + /** + * Look up an actor implementation for an actorID. Will search + * all the actor pools registered with the connection. + * + * @param aActorID string + * Actor ID to look up. + */ + getActor: function DSC_getActor(aActorID) { + let pool = this.poolFor(aActorID); + if (pool) { + return pool.get(aActorID); + } + + if (aActorID === "root") { + return this.rootActor; + } + + return null; + }, + + poolFor: function DSC_actorPool(aActorID) { + if (this._actorPool && this._actorPool.has(aActorID)) { + return this._actorPool; + } + + for (let pool of this._extraPools) { + if (pool.has(aActorID)) { + return pool; + } + } + return null; + }, + + _unknownError: function DSC__unknownError(aPrefix, aError) { + let errorString = safeErrorString(aError); + errorString += "\n" + aError.stack; + // Cu.reportError(errorString); + dumpn(errorString); + return { + error: "unknownError", + message: (aPrefix + "': " + errorString) + }; + }, + + // Transport hooks. + + /** + * Called by DebuggerTransport to dispatch incoming packets as appropriate. + * + * @param aPacket object + * The incoming packet. + */ + onPacket: function DSC_onPacket(aPacket) { + let actor = this.getActor(aPacket.to); + if (!actor) { + this.transport.send({ from: aPacket.to ? aPacket.to : "root", + error: "noSuchActor" }); + return; + } + + // Dyamically-loaded actors have to be created lazily. + if (typeof actor == "function") { + let instance; + try { + instance = new actor(); + } catch (e) { + this.transport.send(this._unknownError( + "Error occurred while creating actor '" + actor.name, + e)); + } + instance.parentID = actor.parentID; + // We want the newly-constructed actor to completely replace the factory + // actor. Reusing the existing actor ID will make sure ActorPool.addActor + // does the right thing. + instance.actorID = actor.actorID; + actor.registeredPool.addActor(instance); + actor = instance; + } + + var ret = null; + // log("actor.requestTypes: "+actor.requestTypes+", cb: "+actor.requestTypes[aPacket.type]); + // Dispatch the request to the actor. + if (actor.requestTypes && actor.requestTypes[aPacket.type]) { + try { + this.currentPacket = aPacket; + ret = actor.requestTypes[aPacket.type].bind(actor)(aPacket, this); + } catch(e) { + this.transport.send(this._unknownError( + "error occurred while processing '" + aPacket.type, + e)); + } finally { + delete this.currentPacket; + } + } else { + ret = { error: "unrecognizedPacketType", + message: ('Actor "' + actor.actorID + + '" does not recognize the packet type "' + + aPacket.type + '"') }; + } + + if (!ret) { + // This should become an error once we've converted every user + // of this to promises in bug 794078. + return; + } + + resolve(ret) + .then(null, (e) => { + return this._unknownError( + "error occurred while processing '" + aPacket.type, + e); + }) + .then(function (aResponse) { + if (!aResponse.from) { + aResponse.from = aPacket.to; + } + return aResponse; + }) + .then(this.transport.send.bind(this.transport)); + }, + + /** + * Called by DebuggerTransport when the underlying stream is closed. + * + * @param aStatus nsresult + * The status code that corresponds to the reason for closing + * the stream. + */ + onClosed: function DSC_onClosed(aStatus) { + dumpn("Cleaning up connection."); + + this._actorPool.cleanup(); + this._actorPool = null; + this._extraPools.map(function(p) { p.cleanup(); }); + this._extraPools = null; + + DebuggerServer._connectionClosed(this); + }, + + /* + * Debugging helper for inspecting the state of the actor pools. + */ + _dumpPools: function DSC_dumpPools() { + dumpn("/-------------------- dumping pools:"); + if (this._actorPool) { + dumpn("--------------------- actorPool actors: " + + uneval(Object.keys(this._actorPool._actors))); + } + for each (let pool in this._extraPools) + dumpn("--------------------- extraPool actors: " + + uneval(Object.keys(pool._actors))); + }, + + /* + * Debugging helper for inspecting the state of an actor pool. + */ + _dumpPool: function DSC_dumpPools(aPool) { + dumpn("/-------------------- dumping pool:"); + dumpn("--------------------- actorPool actors: " + + uneval(Object.keys(aPool._actors))); + } +}; + +/** + * Localization convenience methods. + */ +// let L10N = { + +// /** +// * L10N shortcut function. +// * +// * @param string aName +// * @return string +// */ +// getStr: function L10N_getStr(aName) { +// return this.stringBundle.GetStringFromName(aName); +// } +// }; + +// XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() { +// return Services.strings.createBundle(DBG_STRINGS_URI); +// }); diff --git a/cocos/scripting/javascript/bindings/js/debugger/transport.js b/cocos/scripting/javascript/bindings/js/debugger/transport.js new file mode 100644 index 0000000000..8676e367a8 --- /dev/null +++ b/cocos/scripting/javascript/bindings/js/debugger/transport.js @@ -0,0 +1,288 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +// Components.utils.import("resource://gre/modules/NetUtil.jsm"); + +/** + * An adapter that handles data transfers between the debugger client and + * server. It can work with both nsIPipe and nsIServerSocket transports so + * long as the properly created input and output streams are specified. + * + * @param aInput nsIInputStream + * The input stream. + * @param aOutput nsIAsyncOutputStream + * The output stream. + * + * Given a DebuggerTransport instance dt: + * 1) Set dt.hooks to a packet handler object (described below). + * 2) Call dt.ready() to begin watching for input packets. + * 3) Send packets as you please, and handle incoming packets passed to + * hook.onPacket. + * 4) Call dt.close() to close the connection, and disengage from the event + * loop. + * + * A packet handler object is an object with two methods: + * + * - onPacket(packet) - called when we have received a complete packet. + * |Packet| is the parsed form of the packet --- a JavaScript value, not + * a JSON-syntax string. + * + * - onClosed(status) - called when the connection is closed. |Status| is + * an nsresult, of the sort passed to nsIRequestObserver. + * + * Data is transferred as a JSON packet serialized into a string, with the + * string length prepended to the packet, followed by a colon + * ([length]:[packet]). The contents of the JSON packet are specified in + * the Remote Debugging Protocol specification. + */ +this.DebuggerTransport = function DebuggerTransport(aInput, aOutput) +{ + this._input = aInput; + this._output = aOutput; + + this._converter = null;//Cc["@mozilla.org/intl/scriptableunicodeconverter"] + // .createInstance(Ci.nsIScriptableUnicodeConverter); + // this._converter.charset = "UTF-8"; + + this._outgoing = ""; + this._incoming = ""; + + this.hooks = null; +} + +DebuggerTransport.prototype = { + /** + * Transmit a packet. + * + * This method returns immediately, without waiting for the entire + * packet to be transmitted, registering event handlers as needed to + * transmit the entire packet. Packets are transmitted in the order + * they are passed to this method. + */ + send: function DT_send(aPacket) { + // TODO (bug 709088): remove pretty printing when the protocol is done. + let data = JSON.stringify(aPacket, null, 2); + // data = this._converter.ConvertFromUnicode(data); + + let data_for_len = utf16to8(data); + + this._outgoing = data_for_len.length + ':' + data; + + this._flushOutgoing(); + }, + + /** + * Close the transport. + */ + close: function DT_close() { + this._input.close(); + this._output.close(); + }, + + /** + * Flush the outgoing stream. + */ + _flushOutgoing: function DT_flushOutgoing() { + if (this._outgoing.length > 0) { + // var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + // this._output.asyncWait(this, 0, 0, threadManager.currentThread); + log("outgoing: " + this._outgoing);//.substring(0, 200)); + _bufferWrite(this._outgoing); + } + }, + + onOutputStreamReady: + makeInfallible(function DT_onOutputStreamReady(aStream) { + let written = 0; + try { + written = aStream.write(this._outgoing, this._outgoing.length); + } catch(e if e.result == Components.results.NS_BASE_STREAM_CLOSED) { + dumpn("Connection closed."); + this.close(); + return; + } + this._outgoing = this._outgoing.slice(written); + this._flushOutgoing(); + }, "DebuggerTransport.prototype.onOutputStreamReady"), + + /** + * Initialize the input stream for reading. Once this method has been + * called, we watch for packets on the input stream, and pass them to + * this.hook.onPacket. + */ + ready: function DT_ready() { + // let pump = Cc["@mozilla.org/network/input-stream-pump;1"] + // .createInstance(Ci.nsIInputStreamPump); + // pump.init(this._input, -1, -1, 0, 0, false); + // pump.asyncRead(this, null); + }, + + // nsIStreamListener + onStartRequest: + makeInfallible(function DT_onStartRequest(aRequest, aContext) {}, + "DebuggerTransport.prototype.onStartRequest"), + + onStopRequest: + makeInfallible(function DT_onStopRequest(aRequest, aContext, aStatus) { + this.close(); + if (this.hooks) { + this.hooks.onClosed(aStatus); + this.hooks = null; + } + }, "DebuggerTransport.prototype.onStopRequest"), + + onDataAvailable: makeInfallible(function DT_onDataAvailable (incoming) +// makeInfallible(function DT_onDataAvailable(aRequest, aContext, +// aStream, aOffset, aCount) + { + this._incoming = incoming;//+= NetUtil.readInputStreamToString(aStream, + // aStream.available()); + while (this._processIncoming()) {}; + }, "DebuggerTransport.prototype.onDataAvailable"), + + /** + * Process incoming packets. Returns true if a packet has been received, either + * if it was properly parsed or not. Returns false if the incoming stream does + * not contain a full packet yet. After a proper packet is parsed, the dispatch + * handler DebuggerTransport.hooks.onPacket is called with the packet as a + * parameter. + */ + _processIncoming: function DT__processIncoming() { + // Well this is ugly. + let sep = this._incoming.indexOf(':'); + if (sep < 0) { + return false; + } + + let count = parseInt(this._incoming.substring(0, sep)); + if (this._incoming.length - (sep + 1) < count) { + // Don't have a complete request yet. + return false; + } + + // We have a complete request, pluck it out of the data and parse it. + this._incoming = this._incoming.substring(sep + 1); + let packet = this._incoming.substring(0, count); + this._incoming = this._incoming.substring(count); + + try { + // packet = this._converter.ConvertToUnicode(packet); + packet = utf8to16(packet); + var parsed = JSON.parse(packet); + } catch(e) { + let msg = "Error parsing incoming packet: " + packet + " (" + e + " - " + e.stack + ")"; + // if (Cu.reportError) { + // Cu.reportError(msg); + // } + dump(msg + "\n"); + return true; + } + + dumpn("Got: " + packet); + let self = this; + + // Services.tm.currentThread.dispatch(makeInfallible(function() { + self.hooks.onPacket(parsed); + // }, "DebuggerTransport instance's this.hooks.onPacket"), 0); + + return true; + } +} + + +/** + * An adapter that handles data transfers between the debugger client and + * server when they both run in the same process. It presents the same API as + * DebuggerTransport, but instead of transmitting serialized messages across a + * connection it merely calls the packet dispatcher of the other side. + * + * @param aOther LocalDebuggerTransport + * The other endpoint for this debugger connection. + * + * @see DebuggerTransport + */ +this.LocalDebuggerTransport = function LocalDebuggerTransport(aOther) +{ + this.other = aOther; + this.hooks = null; + + /* + * A packet number, shared between this and this.other. This isn't used + * by the protocol at all, but it makes the packet traces a lot easier to + * follow. + */ + this._serial = this.other ? this.other._serial : { count: 0 }; +} + +LocalDebuggerTransport.prototype = { + /** + * Transmit a message by directly calling the onPacket handler of the other + * endpoint. + */ + send: function LDT_send(aPacket) { + let serial = this._serial.count++; + if (wantLogging) { + if (aPacket.to) { + dumpn("Packet " + serial + " sent to " + uneval(aPacket.to)); + } else if (aPacket.from) { + dumpn("Packet " + serial + " sent from " + uneval(aPacket.from)); + } + } + this._deepFreeze(aPacket); + let other = this.other; + if (other) { + Services.tm.currentThread.dispatch(makeInfallible(function() { + // Avoid the cost of JSON.stringify() when logging is disabled. + if (wantLogging) { + dumpn("Received packet " + serial + ": " + JSON.stringify(aPacket, null, 2)); + } + if (other.hooks) { + other.hooks.onPacket(aPacket); + } + }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"), 0); + } + }, + + /** + * Close the transport. + */ + close: function LDT_close() { + if (this.other) { + // Remove the reference to the other endpoint before calling close(), to + // avoid infinite recursion. + let other = this.other; + delete this.other; + other.close(); + } + if (this.hooks) { + this.hooks.onClosed(); + this.hooks = null; + } + }, + + /** + * An empty method for emulating the DebuggerTransport API. + */ + ready: function LDT_ready() {}, + + /** + * Helper function that makes an object fully immutable. + */ + _deepFreeze: function LDT_deepFreeze(aObject) { + Object.freeze(aObject); + for (let prop in aObject) { + // Freeze the properties that are objects, not on the prototype, and not + // already frozen. Note that this might leave an unfrozen reference + // somewhere in the object if there is an already frozen object containing + // an unfrozen object. + if (aObject.hasOwnProperty(prop) && typeof aObject === "object" && + !Object.isFrozen(aObject)) { + this._deepFreeze(o[prop]); + } + } + } +};