/**
*
* This is the client code that needs to be implemented into a
* Flash, AIR or FLEX application to collect debug information
* in De MonsterDebugger.
*
* Be aware that any traces made to De MonsterDebugger may
* be viewed by others. De Monster Debugger is intended to be
* used to debug unpublished Flash, AIR of FLEX applications in
* the environment that they will be used in as a final product.
* Please make sure that you do not send any debug material to
* the debugger from a live running application.
*
* Use at your own risk.
*
* @author Ferdi Koomen
* @company De Monsters
* @link http://www.deMonsterDebugger.com
* @version 2.04
*
*
* Special thanks to Arjan van Wijk from MediaMonks.nl
*
*
* Copyright 2009, De Monsters
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
package nl.demonsters.debugger
{
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.display.Sprite;
import flash.events.AsyncErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.events.StatusEvent;
import flash.events.TimerEvent;
import flash.events.Event;
import flash.net.LocalConnection;
import flash.geom.Rectangle;
import flash.utils.ByteArray;
import flash.utils.Timer;
import flash.utils.getTimer;
import flash.utils.describeType;
import flash.system.System;
public class MonsterDebugger
{
// Singleton instance
private static var instance:MonsterDebugger = null;
// Connections
private var lineOut:LocalConnection;
private var lineIn:LocalConnection;
// Connection names
private const LINE_OUT :String = "_debuggerRed";
private const LINE_IN :String = "_debuggerBlue";
// The allow domain for the local connection
// * = Allow communication with all domains
private const ALLOWED_DOMAIN :String = "*";
// Error colors
public static const COLOR_NORMAL :uint = 0x111111;
public static const COLOR_ERROR :uint = 0xFF0000;
public static const COLOR_WARNING :uint = 0xFF3300;
// Commands
private const COMMAND_HELLO :String = "HELLO";
private const COMMAND_ROOT :String = "ROOT";
private const COMMAND_BASE :String = "BASE";
private const COMMAND_TRACE :String = "TRACE";
private const COMMAND_INSPECT :String = "INSPECT";
private const COMMAND_GET_OBJECT :String = "GET_OBJECT";
private const COMMAND_GET_DISPLAYOBJECT :String = "GET_DISPLAYOBJECT";
private const COMMAND_GET_PROPERTIES :String = "GET_PROPERTIES";
private const COMMAND_GET_FUNCTIONS :String = "GET_FUNCTIONS";
private const COMMAND_SET_PROPERTY :String = "SET_PROPERTY";
private const COMMAND_CALL_METHOD :String = "CALL_METHOD";
private const COMMAND_SHOW_HIGHLIGHT :String = "SHOW_HIGHLIGHT";
private const COMMAND_HIDE_HIGHLIGHT :String = "HIDE_HIGHLIGHT";
private const COMMAND_CLEAR_TRACES :String = "CLEAR_TRACES";
private const COMMAND_MONITOR :String = "MONITOR";
private const COMMAND_SNAPSHOT :String = "SNAPSHOT";
private const COMMAND_NOTFOUND :String = "NOTFOUND";
// Types
private const TYPE_VOID :String = "void";
private const TYPE_ARRAY :String = "Array";
private const TYPE_BOOLEAN :String = "Boolean";
private const TYPE_NUMBER :String = "Number";
private const TYPE_OBJECT :String = "Object";
private const TYPE_VECTOR :String = "Vector";
private const TYPE_STRING :String = "String";
private const TYPE_BITMAP :String = "Bitmap";
private const TYPE_BITMAPDATA :String = "BitmapData";
private const TYPE_INT :String = "int";
private const TYPE_UINT :String = "uint";
private const TYPE_XML :String = "XML";
private const TYPE_XMLLIST :String = "XMLList";
private const TYPE_XMLNODE :String = "XMLNode";
private const TYPE_XMLVALUE :String = "XMLValue";
private const TYPE_XMLATTRIBUTE :String = "XMLAttribute";
private const TYPE_METHOD :String = "MethodClosure";
private const TYPE_FUNCTION :String = "Function";
private const TYPE_BYTEARRAY :String = "ByteArray";
private const TYPE_WARNING :String = "Warning";
private const TYPE_DISPLAYOBJECT :String = "DisplayObject";
// Access types
private const ACCESS_VARIABLE :String = "variable";
private const ACCESS_CONSTANT :String = "constant";
private const ACCESS_ACCESSOR :String = "accessor";
private const ACCESS_METHOD :String = "method";
// Permission types
private const PERMISSION_READWRITE :String = "readwrite";
private const PERMISSION_READONLY :String = "readonly";
private const PERMISSION_WRITEONLY :String = "writeonly";
// Icon types
private const ICON_DEFAULT :String = "iconDefault";
private const ICON_ROOT :String = "iconRoot";
private const ICON_WARNING :String = "iconWarning";
private const ICON_VARIABLE :String = "iconVariable";
private const ICON_VARIABLE_READONLY :String = "iconVariableReadonly";
private const ICON_VARIABLE_WRITEONLY :String = "iconVariableWriteonly";
private const ICON_XMLNODE :String = "iconXMLNode";
private const ICON_XMLVALUE :String = "iconXMLValue";
private const ICON_XMLATTRIBUTE :String = "iconXMLAttribute";
private const ICON_FUNCTION :String = "iconFunction";
// Highlight color and border thickness
private const HIGHLIGHT_COLOR :uint = 0xFFFF00;
private const HIGHLIGHT_BORDER :int = 4;
// Max local connection package size
private const MAX_PACKAGE_BYTES :int = 40000;
// Version
private const VERSION:Number = 2.04;
// The root of the application
private var root:Object = null;
// Highlight display object
private var highlight:Sprite = null;
// Timer for the monitor
private var monitor:Timer;
private var monitorTime:uint;
private var monitorFrames:uint;
private var monitorSprite:Sprite;
// Enabled / disabled
public var _enabled:Boolean = true;
/**
* Constructor
* The target can also be a static function
* @param target: The root of the application
*/
public function MonsterDebugger(target:Object = null)
{
// Check if the debugger has already been initialized
if (instance == null)
{
// Save the instance
instance = this;
// Setup line out
lineOut = new LocalConnection();
lineOut.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
lineOut.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
lineOut.addEventListener(StatusEvent.STATUS, statusHandler);
// Setup line in
lineIn = new LocalConnection();
lineIn.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
lineIn.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
lineIn.addEventListener(StatusEvent.STATUS, statusHandler);
lineIn.allowDomain(ALLOWED_DOMAIN);
lineIn.client = this;
// Setup the fps and memory listeners
monitorTime = getTimer();
monitorFrames = 0;
monitorSprite = new Sprite();
monitorSprite.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
monitor = new Timer(500);
monitor.addEventListener(TimerEvent.TIMER, monitorHandler);
monitor.start();
try {
lineIn.connect(LINE_IN);
} catch(error:ArgumentError) {
// Do nothing
}
}
// Save the root
// Send the first message
if (target != MonsterDebugger.singletonCheck) {
instance.root = target;
instance.send({text:COMMAND_HELLO, version:VERSION});
}
}
/**
* This function is called from the AIR application
* @param data: A compressed object containing the commands
*/
public function onReceivedData(data:ByteArray):void
{
if (enabled)
{
//Variables for the commands
var object:*;
var method:Function;
var xml:XML;
// Uncompress the item data
data.uncompress();
// Read the command from the data
var command:Object = data.readObject();
// Do the actions
switch(command["text"])
{
// Save the domain
case COMMAND_HELLO:
send({text:COMMAND_HELLO, version:VERSION});
break;
// Get the base of the application
case COMMAND_ROOT:
object = getObject("", 0);
if (object != null) {
xml = XML(parseObject(object, "", command["functions"], 1, 2));
send({text:COMMAND_ROOT, xml:xml});
if (isDisplayObject(object)) {
xml = XML(parseDisplayObject(object, "", command["functions"], 1, 2));
send({text:COMMAND_BASE, xml:xml});
}
}
break;
// Return the parsed object
case COMMAND_GET_OBJECT:
object = getObject(command["target"], 0);
if (object != null) {
xml = XML(parseObject(object, command["target"], command["functions"], 1, 2));
send({text:COMMAND_GET_OBJECT, xml:xml});
}
break;
// Return the parsed object
case COMMAND_GET_DISPLAYOBJECT:
object = getObject(command["target"], 0);
if (object != null) {
if (isDisplayObject(object)) {
xml = XML(parseDisplayObject(object, command["target"], command["functions"], 1, 2));
send({text:COMMAND_GET_DISPLAYOBJECT, xml:xml});
}
}
break;
// Return a list of functions
case COMMAND_GET_PROPERTIES:
object = getObject(command["target"], 0);
if (object != null) {
xml = XML(parseObject(object, command["target"], false, 1, 1));
send({text:COMMAND_GET_PROPERTIES, xml:xml});
}
break;
// Return a list of functions
case COMMAND_GET_FUNCTIONS:
object = getObject(command["target"], 0);
if (object != null) {
xml = XML(getFunctions(object, command["target"]));
send({text:COMMAND_GET_FUNCTIONS, xml:xml});
}
break;
// Adjust a property and return the value
case COMMAND_SET_PROPERTY:
object = getObject(command["target"], 1);
if (object != null) {
try {
object[command["name"]] = command["value"];
send({text:COMMAND_SET_PROPERTY, value:object[command["name"]]});
} catch (error1:Error) {
send({text:COMMAND_NOTFOUND, target:command["target"]});
break;
}
}
break;
// Call a method and return the answer
case COMMAND_CALL_METHOD:
method = getObject(command["target"], 0);
if (method != null) {
if (command["returnType"] == TYPE_VOID) {
method.apply(this, command["arguments"]);
} else {
object = method.apply(this, command["arguments"]);
xml = XML(parseObject(object, "", false, 1, 4));
send({text:COMMAND_CALL_METHOD, id:command["id"], xml:xml});
}
}
break;
// Remove and add a new highlight if posible
case COMMAND_SHOW_HIGHLIGHT:
if (highlight != null) {
try {
highlight.parent.removeChild(highlight);
highlight = null;
} catch(error2:Error) {
//
}
}
object = getObject(command["target"], 0);
if (isDisplayObject(object) && isDisplayObject(object["parent"])) {
var bounds:Rectangle = object.getBounds(object["parent"]);
highlight = new Sprite();
highlight.x = 0;
highlight.y = 0;
highlight.graphics.beginFill(0, 0);
highlight.graphics.lineStyle(HIGHLIGHT_BORDER, HIGHLIGHT_COLOR);
highlight.graphics.drawRect(bounds.x, bounds.y, bounds.width, bounds.height);
highlight.graphics.endFill();
highlight.mouseChildren = false;
highlight.mouseEnabled = false;
try {
object["parent"].addChild(highlight);
} catch(error3:Error) {
highlight = null;
}
}
break;
// Remove the highlight
case COMMAND_HIDE_HIGHLIGHT:
if (highlight != null) {
try {
highlight.parent.removeChild(highlight);
highlight = null;
} catch(error4:Error) {
//
}
}
break;
}
}
}
/**
* The actual send function
* @param data: The raw uncompressed data to send
*/
private function send(data:Object):void
{
if (enabled)
{
// Compress the data
var item:ByteArray = new ByteArray();
item.writeObject(data);
item.compress();
// Array to hold the data packages
var dataPackages:Array = new Array();
// Counter for the loops
var i:int = 0;
// Check if the data should be splitted
// The max size for localconnection = 40kb = 40960b
// We use 960b for the package definition
if (item.length > MAX_PACKAGE_BYTES)
{
// Save the length
var bytesAvailable:int = item.length;
var offset:int = 0;
// Calculate the total package count
var total:int = Math.ceil(item.length / MAX_PACKAGE_BYTES);
// Loop through the bytes / chunks
for (i = 0; i < total; i++)
{
// Set the length to read
var length:int = bytesAvailable;
if (length > MAX_PACKAGE_BYTES) {
length = MAX_PACKAGE_BYTES;
}
// Read a chunk of data
var tmp:ByteArray = new ByteArray();
tmp.writeBytes(item, offset, length);
// Create a data package
dataPackages.push({total:total, nr:(i + 1), bytes:tmp});
// Update the bytes available and offset
bytesAvailable -= length;
offset += length;
}
}
else
{
// The data size is under 40kb, so just send one package
dataPackages.push({total:1, nr:1, bytes:item});
}
// send the data packages through the line out
for (i = 0; i < dataPackages.length; i++) {
try {
lineOut.send(LINE_OUT, "onReceivedData", dataPackages[i]);
} catch (error:Error) {
break;
}
}
}
}
/**
* Set another target in the or window
* @param target: The target to display in the or
*/
public static function inspect(target:Object):void
{
// Check if the debugger has been enabled
if (instance != null && target != null)
{
// Set the new root
instance.root = target;
// Parse the new target
// Send the new XML
var object:* = instance.getObject("", 0);
if (object != null) {
var xml:XML = XML(instance.parseObject(object, "", false, 1, 2));
instance.send({text:instance.COMMAND_INSPECT, xml:xml});
if (instance.isDisplayObject(object)) {
xml = XML(instance.parseDisplayObject(object, "", false, 1, 2));
instance.send({text:instance.COMMAND_BASE, xml:xml});
}
}
}
}
/**
* Static snapshot function
* @param target: The target from where the snapshot is called
* @param object: The object to snapshot
* @param color: The color in the interface
*/
public static function snapshot(target:DisplayObject, color:uint = 0x111111):void
{
if (instance == null) instance = new MonsterDebugger(MonsterDebugger.singletonCheck);
if (MonsterDebugger.enabled) instance.snapshotInternal(target, color);
}
/**
* Private snapshot function
* @param target: The target from where the snapshot is called
* @param object: The object to snapshot
* @param color: The color in the interface
*/
private function snapshotInternal(target:DisplayObject, color:uint = 0x111111):void
{
if (enabled)
{
// Create the bitmapdata
var bitmapData:BitmapData = new BitmapData(target.width, target.height);
bitmapData.draw(target);
// Write the bitmap in the bytearray
var bytes:ByteArray = bitmapData.getPixels(new Rectangle(0, 0, target.width, target.height));
//Create a send object
send({text:COMMAND_SNAPSHOT, date:new Date(), target:String(target), bytes:bytes, width:target.width, height:target.height, color:color});
// Clear the data
bitmapData.dispose();
bytes = null;
}
}
/**
* Static trace function
* @param target: The target from where the trace is called
* @param object: The object to trace
* @param color: The color of the trace in the interface
* @parem functions: Include or exclude the functions
* @param depth: The maximum depth of the trace
*/
public static function trace(target:Object, object:*, color:uint = 0x111111, functions:Boolean = false, depth:int = 4):void
{
if (instance == null) instance = new MonsterDebugger(MonsterDebugger.singletonCheck);
if (MonsterDebugger.enabled) instance.traceInternal(target, object, color, functions, depth);
}
/**
* Private trace function
* @param target: The target from where the trace is called
* @param object: The object to trace
* @param color: The color of the trace in the interface
* @parem functions: Include or exclude the functions
* @param depth: The maximum depth of the trace
*/
private function traceInternal(target:Object, object:*, color:uint = 0x111111, functions:Boolean = false, depth:int = 4):void
{
if (enabled)
{
// Get the object information
var xml:XML = XML(parseObject(object, "", functions, 1, depth));
//Create a send object
send({text:COMMAND_TRACE, date:new Date(), target:String(target), xml:xml, color:color});
}
}
/**
* Static clear traces function
* This clears the traces in the application
*/
public static function clearTraces():void
{
if (instance == null) instance = new MonsterDebugger(MonsterDebugger.singletonCheck);
if (MonsterDebugger.enabled) instance.clearTracesInternal();
}
/**
* Private clear traces function
* This clears the traces in the application
*/
private function clearTracesInternal():void
{
if (enabled)
{
//Create a send object
send({text:COMMAND_CLEAR_TRACES});
}
}
/**
* Check if an object is drawable displayobject
* @param object: The object to check
*/
private function isDisplayObject(object:*):Boolean
{
return (object is DisplayObject || object is DisplayObjectContainer);
}
/**
* Return an object
* @param target: A point seperated path to the object
* @param parent: Number of parents
*/
private function getObject(target:String = "", parent:int = 0):*
{
// Object to return
var object:* = instance.root;
// Check if the path is not empty
if (target != "")
{
// Split the path
var splitted:Array = target.split(".");
// Loop through the array
for (var i:int = 0; i < splitted.length - parent; i++)
{
// Check if the string isn't empty
if (splitted[i] != "")
{
try
{
// Check if we should call the XML children function()
// Or the getChildAt function
// If not: Just update the path to the object
if (splitted[i] == "children()")
{
object = object.children();
}
else if (splitted[i].indexOf("getChildAt(") == 0)
{
var index:Number = splitted[i].substring(11, splitted[i].indexOf(")", 11));
object = DisplayObjectContainer(object).getChildAt(index);
}
else
{
object = object[splitted[i]];
}
}
catch (error:ReferenceError)
{
// The object is not found
send({text:COMMAND_NOTFOUND, target:target});
break;
}
}
}
}
// Return the object
return object;
}
/**
* Get the functions of an object
* @param object: The object to parse
* @param target: A point seperated path to the object
*/
private function getFunctions(object:*, target:String = ""):String
{
// The return string
var xml:String = "";
// Create the opening node
xml += createNode("root");
try
{
// Get the descriptor
var description:XML = describeType(object);
var type:String = parseType(description.@name);
var childType:String = "";
var childName:String = "";
var childTarget:String = "";
var methods:XMLList = description..method;
var methodsArr:Array = new Array();
var returnType:String;
var parameters:XMLList;
var args:Array;
var argsString:String;
var optional:Boolean = false;
var double:Boolean = false;
var i:int = 0;
var n:int = 0;
// Create the head node
xml += createNode("node", {icon:ICON_DEFAULT, label:"(" + type + ")", target:target});
// Save the methods
// Filter out doubles (this should not happen though)
for (i = 0; i < methods.length(); i++) {
for (n = 0; n < methodsArr.length; n++) {
if (methodsArr[n].name == methods[i].@name) {
double = true;
break;
}
}
if (!double) {
methodsArr.push({name:methods[i].@name, xml:methods[i], access:ACCESS_METHOD});
}
}
// Sort the nodes
methodsArr.sortOn("name");
// Loop through the methods
for (i = 0; i < methodsArr.length; i++)
{
// Save the type
childType = TYPE_FUNCTION;
childName = methodsArr[i].xml.@name;
childTarget = target + "." + childName;
// Save the function info
// Parameters, arguments, return type, etc
returnType = parseType(methodsArr[i].xml.@returnType);
parameters = methodsArr[i].xml..parameter;
args = new Array();
argsString = "";
optional = false;
// Create the parameters
for (n = 0; n < parameters.length(); n++)
{
// Optional parameters should start with a bracket
if (parameters[n].@optional == "true" && !optional){
optional = true;
args.push("[");
}
// Push the parameter
args.push(parseType(parameters[n].@type));
}
// The optional bracket is needed
if (optional) {
args.push("]");
}
// Create the arguments string
argsString = args.join(", ");
argsString = argsString.replace("[, ", "[");
argsString = argsString.replace(", ]", "]");
// Create the node
xml += createNode("node", {
icon: ICON_FUNCTION,
label: childName + "(" + argsString + "):" + returnType,
args: argsString,
name: childName,
type: TYPE_FUNCTION,
access: ACCESS_METHOD,
returnType: returnType,
target: childTarget
});
// Loop through the parameters
for (n = 0; n < parameters.length(); n++)
{
// Create the node
xml += createNode("parameter", {
type: parseType(parameters[n].@type),
index: parameters[n].@index,
optional: parameters[n].@optional
}, true);
}
// Close the function node
xml += createNode("/node");
}
// Create the head node
xml += createNode("/node");
}
catch (error:Error)
{
// The object is not found
var msg:String = "";
msg += createNode("root");
msg += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Not found", name:"Not found"}, true);
msg += createNode("/root");
send({text:COMMAND_NOTFOUND, target:target, xml:XML(msg)});
}
// Create a closing node
xml += createNode("/root");
// Return the xml
return xml;
}
/**
* Parse an object
* @param object: The object to parse
* @param target: A point seperated path to the object
* @param functions: Include or exclude functions
* @param currentDepth: The current trace depth
* @param maxDepth:: The maximum trace depth
*/
private function parseObject(object:*, target:String = "", functions:Boolean = false, currentDepth:int = 1, maxDepth:int = 4):String
{
// Variables needed in the loops
var xml:String = "";
var childType:String = "";
var childName:String = "";
var childTarget:String = "";
var description:XML = new XML();
var type:String = "";
var base:String = "";
var isXML:Boolean = false;
var isXMLString:XML;
var i:int = 0;
var n:int = 0;
// Check if the max trace depth is reached
if (maxDepth == -1 || currentDepth <= maxDepth)
{
// Create the opening node if needed
if (currentDepth == 1) xml += createNode("root");
try
{
// Get the descriptor
description = describeType(object);
type = parseType(description.@name);
base = parseType(description.@base);
if (functions && base == TYPE_FUNCTION)
{
// Trace an empty function
xml += createNode("node", {
icon: ICON_FUNCTION,
label: "(Function)",
name: "",
type: TYPE_FUNCTION,
value: "",
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
else if (type == TYPE_ARRAY || type == TYPE_VECTOR)
{
// Add data description if needed
if (currentDepth == 1) xml += createNode("node", {icon:ICON_ROOT, label:"(" + type + ")", target:target});
// Create the length property
// The only property of the array
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "length" + " (" + TYPE_UINT + ") = " + object["length"],
name: "length",
type: TYPE_UINT,
value: object["length"],
target: target + "." + "length",
access: ACCESS_VARIABLE,
permission: PERMISSION_READONLY
}, true);
// Get and sort the properties
var keys:Array = new Array();
for (var key:* in object) {
keys.push(key);
}
keys.sort();
// Loop through the array
for (i = 0; i < keys.length; i++)
{
// Save the type
childType = parseType(describeType(object[keys[i]]).@name);
childTarget = target + "." + String(keys[i]);
// Check if we can create a single string or a new node
if (childType == TYPE_STRING || childType == TYPE_BOOLEAN || childType == TYPE_NUMBER || childType == TYPE_INT || childType == TYPE_UINT || childType == TYPE_FUNCTION)
{
isXML = false;
isXMLString = new XML();
// Check if the string is a XML string
if (childType == TYPE_STRING) {
try {
isXMLString = new XML(object[keys[i]]);
if (!isXMLString.hasSimpleContent() && isXMLString.children().length() > 0) isXML = true;
} catch(error1:TypeError) {}
}
try {
if (!isXML) {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "[" + keys[i] + "] (" + childType + ") = " + printObject(object[keys[i]], childType),
name: "[" + keys[i] + "]",
type: childType,
value: printObject(object[keys[i]], childType),
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
} else {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "[" + keys[i] + "] (" + childType + ")",
name: "[" + keys[i] + "]",
type: childType,
value: "",
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, false);
xml += parseXML(isXMLString, childTarget + "." + "cildren()", currentDepth, maxDepth);
xml += createNode("/node");
}
} catch(error2:Error) {}
}
else
{
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "[" + keys[i] + "] (" + childType + ")",
name: "[" + keys[i] + "]",
type: childType,
value: "",
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
});
try
{
// Try to parse the object
xml += parseObject(object[keys[i]], childTarget, functions, currentDepth + 1, maxDepth);
}
catch(error3:Error)
{
// If this fails add a warning message for the user
xml += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Unreadable", name:"Unreadable"}, true);
}
xml += createNode("/node");
}
}
// Close data description if needed
if (currentDepth == 1) xml += createNode("/node");
}
else if (type == TYPE_OBJECT)
{
// Add data description if needed
if (currentDepth == 1) xml += createNode("node", {icon:ICON_ROOT, label:"(" + type + ")", target:target});
// Get and sort the properties
var properties:Array = new Array();
for (var prop:* in object) {
properties.push(prop);
}
properties.sort();
// Loop through the array
for (i = 0; i < properties.length; i++)
{
// Save the type
childType = parseType(describeType(object[properties[i]]).@name);
childTarget = target + "." + properties[i];
// Check if we can create a single string or a new node
if (childType == TYPE_STRING || childType == TYPE_BOOLEAN || childType == TYPE_NUMBER || childType == TYPE_INT || childType == TYPE_UINT || childType == TYPE_FUNCTION)
{
isXML = false;
isXMLString = new XML();
// Check if the string is a XML string
if (childType == TYPE_STRING) {
try {
isXMLString = new XML(object[properties[i]]);
if (!isXMLString.hasSimpleContent() && isXMLString.children().length() > 0) isXML = true;
} catch(error4:TypeError) {}
}
try {
if (!isXML) {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: properties[i] + " (" + childType + ") = " + printObject(object[properties[i]], childType),
name: properties[i],
type: childType,
value: printObject(object[properties[i]], childType),
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
} else {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: properties[i] + " (" + childType + ")",
name: properties[i],
type: childType,
value: "",
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, false);
xml += parseXML(isXMLString, childTarget + "." + "cildren()", currentDepth, maxDepth);
xml += createNode("/node");
}
} catch(error5:Error) {}
}
else
{
xml += createNode("node", {
icon: ICON_VARIABLE,
label: properties[i] + " (" + childType + ")",
name: properties[i],
type: childType,
value: "",
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
});
try
{
// Try to parse the object
xml += parseObject(object[properties[i]], childTarget, functions, currentDepth + 1, maxDepth);
}
catch(error6:Error)
{
// If this fails add a warning message for the user
xml += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Unreadable", name:"Unreadable"}, true);
}
xml += createNode("/node");
}
}
// Close data description if needed
if (currentDepth == 1) xml += createNode("/node");
}
else if (type == TYPE_XML)
{
// Add data description if needed
if (currentDepth == 1) xml += createNode("node", {icon:ICON_ROOT, label:"(" + type + ")", target:target});
// Parse the XML
xml += parseXML(object, target + "." + "cildren()", currentDepth, maxDepth);
// Close data description if needed
if (currentDepth == 1) xml += createNode("/node");
}
else if (type == TYPE_XMLLIST)
{
// Add data description if needed
if (currentDepth == 1) xml += createNode("node", {icon:ICON_ROOT, label:"(" + type + ")", target:target});
// Create the length property
// The only property of the array
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "length" + " (" + TYPE_UINT + ") = " + object.length(),
name: "length",
type: TYPE_UINT,
value: object.length(),
target: target + "." + "length",
access: ACCESS_VARIABLE,
permission: PERMISSION_READONLY
}, true);
// Loop through the xml nodes
for (i = 0; i < object.length(); i++)
{
xml += parseXML(object[i], target + "." + String(i) + ".children()", currentDepth, maxDepth);
}
// Close data description if needed
if (currentDepth == 1) xml += createNode("/node");
}
else if (type == TYPE_STRING || type == TYPE_BOOLEAN || type == TYPE_NUMBER || type == TYPE_INT || type == TYPE_UINT)
{
isXML = false;
isXMLString = new XML();
// Check if the string is a XML string
if (type == TYPE_STRING) {
try {
isXMLString = new XML(object);
if (!isXMLString.hasSimpleContent() && isXMLString.children().length() > 0) isXML = true;
} catch(error7:TypeError) {}
}
try {
if (!isXML) {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "(" + type + ") = " + printObject(object, type),
name: "",
type: type,
value: printObject(object, type),
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
} else {
xml += createNode("node", {
icon: ICON_VARIABLE,
label: "(" + type + ")",
name: "",
type: type,
value: "",
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, false);
xml += parseXML(isXMLString, target + "." + "cildren()", currentDepth, maxDepth);
xml += createNode("/node");
}
} catch(error8:Error) {}
}
else
{
// Add data description if needed
if (currentDepth == 1) xml += createNode("node", {icon:ICON_ROOT, label:"(" + type + ")", target:target});
// Get the data
var variables:XMLList = description..variable;
var accessors:XMLList = description..accessor;
var constants:XMLList = description..constant;
var methods:XMLList = description..method;
var variablesArr:Array = new Array();
var methodsArr:Array = new Array();
var double:Boolean = false;
var permission:String = "";
var icon:String = "";
// Save the variables
double = false;
for (i = 0; i < variables.length(); i++) {
for (n = 0; n < variablesArr.length; n++) {
if (variablesArr[n].name == variables[i].@name) {
double = true;
break;
}
}
if (!double) {
variablesArr.push({name:variables[i].@name, xml:variables[i], access:ACCESS_VARIABLE});
}
}
// Save the accessors
double = false;
for (i = 0; i < accessors.length(); i++) {
for (n = 0; n < variablesArr.length; n++) {
if (variablesArr[n].name == accessors[i].@name) {
double = true;
break;
}
}
if (!double) {
variablesArr.push({name:accessors[i].@name, xml:accessors[i], access:ACCESS_ACCESSOR});
}
}
// Save the constants
double = false;
for (i = 0; i < constants.length(); i++) {
for (n = 0; n < variablesArr.length; n++) {
if (variablesArr[n].name == constants[i].@name) {
double = true;
break;
}
}
if (!double) {
variablesArr.push({name:constants[i].@name, xml:constants[i], access:ACCESS_CONSTANT});
}
}
// Save the methods
double = false;
for (i = 0; i < methods.length(); i++) {
for (n = 0; n < methodsArr.length; n++) {
if (methodsArr[n].name == methods[i].@name) {
double = true;
break;
}
}
if (!double) {
methodsArr.push({name:methods[i].@name, xml:methods[i], access:ACCESS_METHOD});
}
}
// Sort the nodes
variablesArr.sortOn("name");
methodsArr.sortOn("name");
// VARIABLES
for (i = 0; i < variablesArr.length; i++)
{
// Save the type
childType = parseType(variablesArr[i].xml.@type);
childName = variablesArr[i].xml.@name;
childTarget = target + "." + childName;
// Save the permission and icon
permission = PERMISSION_READWRITE;
icon = ICON_VARIABLE;
// Check for read / write permissions
if (variablesArr[i].access == ACCESS_CONSTANT) {
// Constant
permission = PERMISSION_READONLY;
icon = ICON_VARIABLE_READONLY;
}
if (variablesArr[i].xml.@access == PERMISSION_READONLY) {
// Only a getter
permission = PERMISSION_READONLY;
icon = ICON_VARIABLE_READONLY;
}
if (variablesArr[i].xml.@access == PERMISSION_WRITEONLY) {
// Only a setter
permission = PERMISSION_WRITEONLY;
icon = ICON_VARIABLE_WRITEONLY;
}
// Don't include write only accessor
if (permission != PERMISSION_WRITEONLY)
{
// Check if we can create a single string or a new node
if (childType == TYPE_STRING || childType == TYPE_BOOLEAN || childType == TYPE_NUMBER || childType == TYPE_INT || childType == TYPE_UINT || childType == TYPE_FUNCTION)
{
isXML = false;
isXMLString = new XML();
// Check if the string is a XML string
if (childType == TYPE_STRING) {
try {
isXMLString = new XML(object[childName]);
if (!isXMLString.hasSimpleContent() && isXMLString.children().length() > 0) isXML = true;
} catch(error9:TypeError) {}
}
try {
if (!isXML) {
xml += createNode("node", {
icon: icon,
label: childName + " (" + childType + ") = " + printObject(object[childName], childType),
name: childName,
type: childType,
value: printObject(object[childName], childType),
target: childTarget,
access: variablesArr[i].access,
permission: permission
}, true);
} else {
xml += createNode("node", {
icon: icon,
label: childName + " (" + childType + ")",
name: childName,
type: childType,
value: "",
target: childTarget,
access: variablesArr[i].access,
permission: permission
}, false);
xml += parseXML(isXMLString, childTarget + "." + "cildren()", currentDepth, maxDepth);
xml += createNode("/node");
}
} catch(error10:Error) {}
}
else
{
xml += createNode("node", {
icon: icon,
label: childName + " (" + childType + ")",
name: childName,
type: childType,
target: childTarget,
access: variablesArr[i].access,
permission: permission
});
try
{
// Try to parse the object
xml += parseObject(object[childName], childTarget, functions, currentDepth + 1, maxDepth);
}
catch(error11:Error)
{
// If this fails add a warning message for the user
xml += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Unreadable", name:"Unreadable"}, true);
}
xml += createNode("/node");
}
}
}
// METHODS
if (functions)
{
for (i = 0; i < methodsArr.length; i++)
{
// Save the type
childType = TYPE_FUNCTION;
childName = methodsArr[i].xml.@name;
childTarget = target + "." + childName;
// Save the parameters
var returnType:String = parseType(methodsArr[i].xml.@returnType);
var parameters:XMLList = methodsArr[i].xml..parameter;
var args:Array = new Array();
// Create the parameters
for (n = 0; n < parameters.length(); n++) {
args.push(parseType(parameters[n].@type));
}
// Create the node
xml += createNode("node", {
icon: ICON_FUNCTION,
label: childName + "(" + args.join(", ") + "):" + returnType,
args: args.join(", "),
name: childName,
type: TYPE_FUNCTION,
access: variablesArr[i].access,
returnType: returnType,
target: childTarget
}, true);
}
}
// Close data description if needed
if (currentDepth == 1) xml += createNode("/node");
}
}
catch (error12:Error)
{
// The object is not found
var msg:String = "";
msg += createNode("root");
msg += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Not found", name:"Not found"}, true);
msg += createNode("/root");
send({text:COMMAND_NOTFOUND, target:target, xml:XML(msg)});
}
// Create a closing node if needed
if (currentDepth == 1) xml += createNode("/root");
}
//Return the xml
return xml;
}
/**
* Parse a display object
* @param object: The object to parse
* @param target: A point seperated path to the object
* @param functions: Include or exclude functions
* @param currentDepth: The current trace depth
* @param maxDepth:: The maximum trace depth
*/
private function parseDisplayObject(object:*, target:String = "", functions:Boolean = false, currentDepth:int = 1, maxDepth:int = 4):String
{
// Variables needed in the loops
var xml:String = "";
var childs:Array;
var child:DisplayObject;
var childType:String = "";
var childIcon:String = "";
var childName:String = "";
var childTarget:String = "";
var childChildren:String = "";
var i:int = 0;
var n:int = 0;
// Check if the max trace depth is reached
if (maxDepth == -1 || currentDepth <= maxDepth)
{
// Create the opening node if needed
if (currentDepth == 1) xml += createNode("root");
try
{
// Add data description if needed
if (currentDepth == 1) {
var ojectName:String = DisplayObject(object).name;
if (ojectName == null || ojectName == "null") {
ojectName = "DisplayObject";
}
xml += createNode("node", {icon:ICON_ROOT, label:"(" + ojectName + ")", target:target});
}
// Get the childs
childs = new Array();
for (i = 0; i < DisplayObjectContainer(object).numChildren; i++) {
childs.push(DisplayObjectContainer(object).getChildAt(i));
}
// Loop through the array
for (i = 0; i < childs.length; i++)
{
// Save the child properties
child = childs[i];
childName = describeType(child).@name;
childType = parseType(childName);
childTarget = target + "." + "getChildAt(" + i + ")";
childIcon = child is DisplayObjectContainer ? ICON_ROOT : ICON_VARIABLE;
childChildren = child is DisplayObjectContainer ? String(DisplayObjectContainer(child).numChildren) : ""
// Create the node
xml += createNode("node", {
icon: childIcon,
label: child.name + " (" + childType + ") " + childChildren,
name: child.name,
type: childType,
value: printObject(child, childType),
target: childTarget,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
});
try
{
// Try to parse the object
xml += parseDisplayObject(child, childTarget, functions, currentDepth + 1, maxDepth);
}
catch(error13:Error)
{
// If this fails add a warning message for the user
xml += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Unreadable", name:"Unreadable"}, true);
}
xml += createNode("/node");
}
// Create a closing node if needed
if (currentDepth == 1) xml += createNode("/node");
}
catch (error14:Error)
{
// The object is not found
var msg:String = "";
msg += createNode("root");
msg += createNode("node", {icon:ICON_WARNING, type:TYPE_WARNING, label:"Not found", name:"Not found"}, true);
msg += createNode("/root");
send({text:COMMAND_NOTFOUND, target:target, xml:XML(msg)});
}
// Create a closing node if needed
if (currentDepth == 1) xml += createNode("/root");
}
//Return the xml
return xml;
}
/**
* Parse a XML node
* @param node: The xml to parse
* @param target: A point seperated path to the object
* @param currentDepth: The current trace depth
* @param maxDepth:: The maximum trace depth
*/
private function parseXML(node:*, target:String = "", currentDepth:int = 1, maxDepth:int = -1):String
{
// Create a return string
var xml:String = "";
var i:int = 0;
// Check if the maximum trace depth is reached
if (maxDepth == -1 || currentDepth <= maxDepth)
{
// Check if the user traced an attribute
if (target.indexOf("@") != -1)
{
// Display a single attribute
xml += createNode("node", {
icon: ICON_XMLATTRIBUTE,
label: node,
name: "",
type: TYPE_XMLATTRIBUTE,
value: node,
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
else if (node.name() == null)
{
// Only a text value
xml += createNode("node", {
icon: ICON_XMLVALUE,
label: "(" + TYPE_XMLVALUE + ") = " + printObject(node, TYPE_XMLVALUE),
name: "",
type: TYPE_XMLVALUE,
value: printObject(node, TYPE_XMLVALUE),
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
else if (node.hasSimpleContent())
{
// Node with one text value and possible attributes
xml += createNode("node", {
icon: ICON_XMLNODE,
label: node.name() + " (" + TYPE_XMLNODE + ")",
name: node.name(),
type: TYPE_XMLNODE,
value: "",
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
});
// Only a text value
if (node != "") {
xml += createNode("node", {
icon: ICON_XMLVALUE,
label: "(" + TYPE_XMLVALUE + ") = " + printObject(node, TYPE_XMLVALUE),
name: "",
type: TYPE_XMLVALUE,
value: printObject(node, TYPE_XMLVALUE),
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
// Loop through the arrributes
for (i = 0; i < node.attributes().length(); i++)
{
xml += createNode("node", {
icon: ICON_XMLATTRIBUTE,
label: "@" + node.attributes()[i].name() + " (" + TYPE_XMLATTRIBUTE + ") = " + node.attributes()[i],
name: "",
type: TYPE_XMLATTRIBUTE,
value: node.attributes()[i],
target: target + "." + "@" + node.attributes()[i].name(),
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
// Close the node
xml += createNode("/node");
}
else
{
// Node with children and attributes
// This node has no value due to the children
xml += createNode("node", {
icon: ICON_XMLNODE,
label: node.name() + " (" + TYPE_XMLNODE + ")",
name: node.name(),
type: TYPE_XMLNODE,
value: "",
target: target,
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
});
// Loop through the arrributes
for (i = 0; i < node.attributes().length(); i++)
{
xml += createNode("node", {
icon: ICON_XMLATTRIBUTE,
label: "@" + node.attributes()[i].name() + " (" + TYPE_XMLATTRIBUTE + ") = " + node.attributes()[i],
name: "",
type: TYPE_XMLATTRIBUTE,
value: node.attributes()[i],
target: target + "." + "@" + node.attributes()[i].name(),
access: ACCESS_VARIABLE,
permission: PERMISSION_READWRITE
}, true);
}
// Loop through children
for (i = 0; i < node.children().length(); i++)
{
var childTarget:String = target + "." + "children()" + "." + i;
xml += parseXML(node.children()[i], childTarget, currentDepth + 1, maxDepth);
}
// Close the node
xml += createNode("/node");
}
}
// Return the formatted XML
return xml;
}
/**
* Converts package names to type
* Example: "nl.demonsters.debugger::MonsterDebugger" becomes "MonsterDebugger"
* We could also use getDefinitionByName() but that can't parse "builtin.as$0::MethodClosure"
* @param type: The string to parse
*/
private function parseType(type:String):String
{
// The return string
var s:String = type;
// Remove the package information if needed
if (type.lastIndexOf("::") != -1) {
s = type.substring(type.lastIndexOf("::") + 2, type.length);
}
// Remove the items after the .
// Vector. becomes Vector
if (s.lastIndexOf(".") != -1) {
s = s.substring(0, s.lastIndexOf("."));
}
// Change "MethodClosure" to "Function"
// This is better for the user interface
if (s == TYPE_METHOD) {
s = TYPE_FUNCTION;
}
// Return the value
return htmlEscape(s);
}
/**
* Create an XML node
* @param title: The title of the node
* @param object: The values of the node
* @param close: Create a closing tag
*/
private function createNode(title:String, object:Object = null, close:Boolean = false):String
{
// The string to store the node
var xml:String = "";
// Open the node
xml += "<" + title;
//Loop through the values
if (object) {
for (var prop:* in object) {
xml += " " + prop + "='" + object[prop] + "'";
}
}
// Close the node
if (close) {
xml += ">" + title + ">";
} else {
xml += ">";
}
// Return the node
return xml;
}
/**
* Print a single object
* @param object: The object to parse
* @param type: The object type
*/
private function printObject(object:*, type:String):String
{
// Create a return string
var s:String = "";
// Check if the object can be converted
// Here we can handle the exceptions
if (type == TYPE_BYTEARRAY)
{
// We dont want to send the complete byte array
// Only display the number of bytes
s = object["length"] + " bytes";
}
else
{
// Display the object
s = htmlEscape(String(object));
}
// Return the printed object
return s;
}
/**
* Send the current FPS
* @param event: Basic event
*/
private function enterFrameHandler(event:Event):void
{
if (enabled) {
monitorFrames++;
}
}
/**
* Send the current memory consumption
* @param event: Basic timer event
*/
private function monitorHandler(event:TimerEvent):void
{
if (enabled)
{
// Get the total Flash Player memory consumption
// Note: This includes all Flash Player instances
var memory:uint = System.totalMemory;
// Calculate the frames pro second
var now:uint = getTimer();
var delta:uint = now - monitorTime;
var fps:uint = monitorFrames / delta * 1000;
monitorFrames = 0;
monitorTime = now;
// Send the data
send({text:COMMAND_MONITOR, memory:memory, fps:fps, time:now, date:new Date()});
}
}
/**
* Converts regular characters to HTML characters
* @param s: The string to convert
*/
private function htmlEscape(s:String):String
{
if (s) {
// Remove single quotes
while(s.indexOf("\'") != -1) {
s = s.replace("\'", "'");
}
// Remove double quotes
while(s.indexOf("\"") != -1) {
s = s.replace("\"", """);
}
var xml:XML = {s};
return xml.toXMLString().replace(/(^)|(<\/a>$)|(^$)/g, "");
} else {
return "";
}
}
/**
* Converts HTML characters to regular characters
* @param s: The string to convert
*/
private function htmlUnescape(s:String):String
{
if (s) {
var xml:XML = ;
xml.replace(0, s);
return String(xml);
} else {
return "";
}
}
/**
* Enable or disable the debugger
*/
public static function get enabled():Boolean
{
if (instance == null) instance = new MonsterDebugger(null);
return instance._enabled;
}
public static function set enabled(value:Boolean):void
{
if (instance == null) instance = new MonsterDebugger(null);
instance._enabled = value;
}
/**
* This function is used for the singleton check in the constructor
* and is given as an argument in the trace and clearTrace function
*/
private static function singletonCheck():void{}
/**
* Event handlers
* Can be used for debugging
*/
private function asyncErrorHandler(event:AsyncErrorEvent):void {}
private function securityErrorHandler(event:SecurityErrorEvent):void {}
private function statusHandler(event:StatusEvent):void {}
}
}