Announce: console.js

Discussion about extending Jaxer through frameworks, extensions and other code libraries

Moderator: Aptana Staff

Announce: console.js

Postby nlsmith » Mon Nov 10, 2008 5:22 pm

I've released a script that partially implements the Firebug Console API using FirePHP for server-side JavaScript. Right now it works on Jaxer.

See http://nlsmith.com/projects/console/

I would be interested in any comments or suggestions.

Thanks,

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby davey » Mon Nov 10, 2008 6:16 pm

Nathan,

i'm downloading it it now. Thanks for this It was on my list of things to tackle.

cheers
davey
User avatar
davey
 
Posts: 710
Joined: Mon Jul 23, 2007 9:25 pm

Postby nlsmith » Mon Nov 10, 2008 6:38 pm

Davey,

davey wrote:Nathan,

i'm downloading it it now. Thanks for this It was on my list of things to tackle.

cheers
davey


A couple of questions:

Which is faster: JSON.encode() or Jaxer.Serialization.toJSONString()? Or is there a difference?

Jaxer seems to not like my method of sending headers for messages that are longer than 5000 characters. If you have some time, could you take a look in the svn repo at test.html and around line 385 in console.js? I think it's correct, and it works on ASP, but Jaxer gives me a 500. I'm going to work on it, but if you could take a look I would appreciate it.

Thanks,

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby davey » Mon Nov 10, 2008 7:13 pm

Very Nice,

first thing I tried was console.log(Jaxer.request) which was too long :-(

but really nice.

You should give some thought to packaging this as a Jaxer extension, then you just need to drop it in the local_jaxer/extensions folder, and it becomes magically available on all serverside pages.

In that case i'd suggest adding it to the Jaxer object as Jaxer.Console. That'd look something like

[codeblock=javascript]
if (typeof Jaxer.console === "undefined") {
/**
* @namespace console
*
* This is a partial implementation of the Firebug Console API using
* the Wildfire/FirePHP protocol.
*
* @see http://nlsmith.com/console/
* @see http://getfirebug.com/console.html
* @see http://www.firephp.org/HQ/Use.htm
*/
var Jaxer.console = (function () {

[/codeblock]

Also from a general debugging perspective, i'd suggest that you adopt the 'named anonymous' function pattern, it looks like redundant code, but it give names to functions in stack traces so you dont see anonymous() as a function listed in a stack trace

so something like the code below...

[codeblock=javascript]
var foo = function() { throw new Error(); }

function bar() { foo(); }

try
{
bar();
}
catch (e)
{
document.write(e.stack);
}

document.write('<BR>');

//redefine foo
var foo = function namedFoo() { throw new Error(); }

try
{
bar();
}
catch (e)
{
document.write(e.stack);
}
[/codeblock]

outputs

Code: Select all
Error()@:0 ()@http://127.0.0.1:8000/console/index.html:5 bar()@http://127.0.0.1:8000/console/index.html:10 @http://127.0.0.1:8000/console/index.html:15

Error()@:0 namedFoo()@http://127.0.0.1:8000/console/index.html:26 bar2()@http://127.0.0.1:8000/console/index.html:31 @http://127.0.0.1:8000/console/index.html:36


note the second stack contains the name 'namedFoo' this can be very useful when debugging.

good stuff.
cheers
User avatar
davey
 
Posts: 710
Joined: Mon Jul 23, 2007 9:25 pm

Postby nlsmith » Mon Nov 10, 2008 7:30 pm

Davey,

davey wrote:Very Nice,

first thing I tried was console.log(Jaxer.request) which was too long :-(


I'm working on that. :)

davey wrote:You should give some thought to packaging this as a Jaxer extension, then you just need to drop it in the local_jaxer/extensions folder, and it becomes magically available on all serverside pages.


I'll get that added.

davey wrote:Also from a general debugging perspective, i'd suggest that you adopt the 'named anonymous' function pattern, it looks like redundant code, but it give names to functions in stack traces so you dont see anonymous() as a function listed in a stack trace


Thanks for the tip and the kind words. I'll work on adding this as well.

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby b1itg » Mon Nov 10, 2008 8:17 pm

Awesome! This will help with development immensely - thanks for sharing this.
User avatar
b1itg
 
Posts: 19
Joined: Sun Jun 24, 2007 5:05 am
Location: Dallas, TX

Postby nlsmith » Tue Nov 11, 2008 3:47 am

davey wrote:You should give some thought to packaging this as a Jaxer extension, then you just need to drop it in the local_jaxer/extensions folder, and it becomes magically available on all serverside pages.


I gave this a try. Jaxer.request and Jaxer.response are both null when the extensions are loaded as far as I can tell. Properties of these are both referenced in the private variables of the console object, so it throws an error.

Any suggestions on an easy way to get around this?

Thanks,

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby nlsmith » Wed Nov 12, 2008 6:10 pm

davey wrote:first thing I tried was console.log(Jaxer.request) which was too long :-(


There actually was a bug with the JSON encoding (I had to use Jaxer.Serialization.toJSONString('...', { as : "JSON" }).)

This is fixed in the current (0.0.2) release.

Jaxer still seems to have a very low limit on header sizes, so it's very easy to log too many messages and have the server not respond.

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby davey » Thu Nov 13, 2008 1:27 am

Nathan,

I took a pass through the code to turn it into a Jaxer extension,

It is now quite different from your original code, i stripped out the non jaxer stuff, as i have no way to test ASP, but I think you should be able follow the way I implemented the jaxer stuff and fold you stuff back in.

Basically I create a Console class, and instantiate that to the Jaxer namespace as Jaxer.console, the methods now use less static data and get things created at runtime, this allows Jaxer.request to be referenced as the code isn't executed until the method is called on the page.

I removed the index var as it would survive across requests, disable now may be have a bit erratically as each instance of the jaxer process may have a different state, it might be better to put that in a session object, or some such thing.

I couldn't quite see how to get the timer stuff to work but jaxer provides Jaxer.Stopwatch, and you could probably just replace the timer code with that, it has a very similar methodology of starting and stopping named timers

I also made the logging calls, write to the jaxer log also, maybe an echo to log setting would be an idea, or when disabled send to jaxer log alone. just thinking out loud.

anyway i'll post the code here and you can review and see if it is useful to you.

Anyway here's the code

[codeblock=javascript]
(function() {

/**
* This is an implementation of the sprintf function based on the one
* found at http://webtoolkit.info. Takes an array instead of
* multiple arguments
*
* @see http://www.webtoolkit.info/javascript-sprintf.html
*/
function sprintf(args) {
if (typeof args == "undefined") { return null; }
if (args.length < 1) { return null; }
if (typeof args[0] != "string") { return null; }
if (typeof RegExp == "undefined") { return null; }

function convert(match, nosign){
if (nosign) {
match.sign = '';
} else {
match.sign = match.negative ? '-' : match.sign;
}
var l = match.min - match.argument.length + 1 - match.sign.length;
var pad = new Array(l < 0 ? 0 : l).join(match.pad);
if (!match.left) {
if (match.pad == "0" || nosign) {
return match.sign + pad + match.argument;
} else {
return pad + match.sign + match.argument;
}
} else {
if (match.pad == "0" || nosign) {
return match.sign + match.argument + pad.replace(/0/g, ' ');
} else {
return match.sign + match.argument + pad;
}
}
}

var string = args[0];
var exp = new RegExp(/(%([%]|(\-)?(\+|\x20)?(0)?(\d+)?(\.(\d)?)?([bcdifosxX])))/g);
var matches = new Array();
var strings = new Array();
var convCount = 0;
var stringPosStart = 0;
var stringPosEnd = 0;
var matchPosEnd = 0;
var newString = '';
var match = null;

while (match = exp.exec(string)) {
if (match[9]) { convCount += 1; }

stringPosStart = matchPosEnd;
stringPosEnd = exp.lastIndex - match[0].length;
strings[strings.length] = string.substring(stringPosStart, stringPosEnd);

matchPosEnd = exp.lastIndex;
matches[matches.length] = {
match: match[0],
left: match[3] ? true : false,
sign: match[4] || '',
pad: match[5] || ' ',
min: match[6] || 0,
precision: match[8],
code: match[9] || '%',
negative: parseInt(args[convCount]) < 0 ? true : false,
argument: String(args[convCount])
};
}
strings[strings.length] = string.substring(matchPosEnd);

if (matches.length == 0) { return string; }
if ((args.length - 1) < convCount) { return null; }

var code = null;
var match = null;
var i = null;

for (i=0; i<matches.length; i++) {

if (matches[i].code == '%') { substitution = '%' }
else if (matches[i].code == 'b') {
matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(2));
substitution = convert(matches[i], true);
}
else if (matches[i].code == 'c') {
matches[i].argument = String(String.fromCharCode(parseInt(Math.abs(parseInt(matches[i].argument)))));
substitution = convert(matches[i], true);
}
else if (matches[i].code == 'd' || matches[i].code == 'i') {
matches[i].argument = String(Math.abs(parseInt(matches[i].argument)));
substitution = convert(matches[i]);
}
else if (matches[i].code == 'f') {
matches[i].argument = String(Math.abs(parseFloat(matches[i].argument)).toFixed(matches[i].precision ? matches[i].precision : 6));
substitution = convert(matches[i]);
}
else if (matches[i].code == 'o') {
matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString( 8 ));
substitution = convert(matches[i]);
}
else if (matches[i].code == 's') {
matches[i].argument = matches[i].argument.substring(0, matches[i].precision ? matches[i].precision : matches[i].argument.length)
substitution = convert(matches[i], true);
}
else if (matches[i].code == 'x') {
matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(16));
substitution = convert(matches[i]);
}
else if (matches[i].code == 'X') {
matches[i].argument = String(Math.abs(parseInt(matches[i].argument)).toString(16));
substitution = convert(matches[i]).toUpperCase();
}
else {
substitution = matches[i].match;
}

newString += strings[i];
newString += substitution;

}
newString += strings[i];

return newString;
}

/**
* Combine the arguments to the function and run them through
* sprintf if necessary
*/
function handleArgs(args) {
args = args || [];
var argc = args.length;
var s = []; // String to return

// Number of items to substitute in first argument
var substitutions = 0;

if (argc > 0) {
if (typeof args[0] === "string") {
substitutions = (args[0].match(/\%\w/g) || []).length + 1;

// Run string through sprintf is needed
if (substitutions > 1) {
s.push(sprintf(args.slice(0, substitutions)));
args = args.slice(substitutions, argc);
argc = args.length;
}
}

for (var i = 0; i < argc; i += 1) {
s.push(String(args[i]));
}

}
return s.join(" ");
}

Console = function Console() {

// should use a Jaxer.Config setting
this.isEnabled = true;

/**
* The maximium length of any given message
*/
this.maxLength = 5000;

/**
* The possible console levels
*/
this.levels = {
log : "LOG",
info : "INFO",
warn : "WARN",
error : "ERROR",
trace : "TRACE",
table : "TABLE",
dump : "DUMP",
group : "GROUP_START",
groupEnd : "GROUP_END",
time : "TIME",
timeEnd : "TIME_END"
};

/**
* Timers for console.time()
* NOTE Jaxer.Stopwatch could be used
*/
this.timers = {};

this.addHeader = function addHeader(header, value) {
Jaxer.response.headers[header] = value;
};

this.toJSON = function toJSON(o) {
return Jaxer.Serialization.toJSONString(o, { as : "JSON" });
};

/**
* The function that does the work of setting the headers and formatting
*/
this.f = function f(level, args) {

if ( !this.hasFirePHP || !this.enabled) { return; }

level = level || this.levels.log;

args = Array.prototype.slice.call(args);

var s = ""; // The string to send to the console
var msg = ""; // The complete header message
var meta = { // Metadata for object
Type : level
};

if (args.length > 0) { // Proceed if there are arguments
// If the first argument is an object, only it gets processed
if (typeof args[0] === "object") {
// Error objects can be handled with more detail if they
// were thrown with the "new Error(...)" constructor
if (this.level === this.levels.error && args[0] instanceof Error) {
if (args[0].lineNumber) {
meta.Line = args[0].lineNumber;
}
if (args[0].fileName) {
meta.File = args[0].fileName;
}
}
s = args[0];
// If the first argument is not an object, we assume it's a
// string or number and process it accordingly
} else {
if (this.level === this.levels.group) { // Handle groups
meta.Label = args[0];
} else if (this.level === this.levels.time) { // Handle time
this.timers[args[0]] = { start : (new Date()).getTime() };
return;
} else if (this.level === this.levels.timeEnd) { // Handle time end
meta.Type = this.levels.info;
if (args[0] in this.timers) {
this.timers[args[0]].end = (new Date()).getTime();
// Calculate elapsed time
args[0] = args[0] + ": " + (this.timers[args[0]].end -
this.timers[args[0]].start) + "ms";
}
}

s = handleArgs(args);
}
} else { return; } // Do nothing if no arguments

// If the starting headers haven't been added, add them

if (!Jaxer.response.headers.hasOwnProperty("X-Wf-Protocol-1")) {

this.addHeader("X-Wf-Protocol-1", "http://meta.wildfirehq.org/Protocol/JsonStream/0.2");
this.addHeader("X-Wf-1-Plugin-1", "http://meta.firephp.org/Wildfire/Plugin/FirePHP/Library-FirePHPCore/0.2.0");
this.addHeader("X-Wf-1-Structure-1", "http://meta.firephp.org/Wildfire/Structure/FirePHP/FirebugConsole/0.1");
}

s = this.toJSON(s); // JSONify string
meta = this.toJSON(meta); // And meta
msg = '[' + meta + ',' + s + ']'; // Start message

if (msg.length < this.maxLength) {
this.addHeader('X-Wf-1-1-1-' + Jaxer.response.headers.__count__, msg.length + '|' + msg + '|');
} else { // Split the message up if it's greater than maxLength
(function splitMessage() {

var key = 'X-Wf-1-1-1-' + Jaxer.response.headers.__count__;
var value = "";
var totalLength = msg.length;
var messages = [];
var chars = msg.split("");
var part = "";

// Split the messages up
for (var i = 0; i < chars.length; i += maxLength) {
part = chars.slice(i, i + maxLength).join("");
messages.push(part);
}

// Add a header for each part
for(i = 0; i < messages.length; i += 1) {

key = key = 'X-Wf-1-1-1-' + Jaxer.response.headers.__count__;
value = '|' + messages[i] + '|';

if (i === 0) { value = totalLength + value; }
if (i !== messages.length - 1) { value += "\\"; }

this.addHeader(key, value);
}
})();
}
};

this.log = function log() {
Jaxer.Log.info(Array.slice(arguments).join(" "));
this.f(this.levels.log, arguments);
};

this.debug = function debug() {
// log & debug do the same thing
Jaxer.Log.info(Array.slice(arguments).join(" "));
this.f(this.levels.log, arguments);
};

this.info = function info() {
Jaxer.Log.info(Array.slice(arguments).join(" "));
this.f(this.levels.info, arguments);
};

this.warn = function warn() {
Jaxer.Log.warn(Array.slice(arguments).join(" "));
this.f(this.levels.warn, arguments);
};

this.error = function error() {
Jaxer.Log.error(Array.slice(arguments).join(" "));
this.f(this.levels.error, arguments);
};

this.group = function group() {
this.f(levels.group, [arguments[0] || ""]);
};

this.groupEnd = function groupEnd() {
this.f(this.levels.groupEnd, [""]);
};

this.time = function time() {
this.f(this.levels.time, [arguments[0] || ""]);
};

this.timeEnd = function timeEnd() {
this.f(this.levels.timeEnd, [arguments[0] || ""]);
};

/**
* TODO make getter/setter
* Enable the console
*/
this.enable = function enable() {
this.enabled = true;
};
/**
* Disable the console
*/
this.disable = function disable() {
this.enabled = false;
};
};

Console.prototype.__defineGetter__("enabled", function enabled() {
return this.isEnabled;
});

Console.prototype.__defineSetter__("enabled", function enabled(state) {
this.f(this.levels.info, ["console is "+((state === true)?"en":"dis")+"abled"]);
return this.isEnabled = (state === true);
});

Console.prototype.__defineGetter__("userAgent", function userAgent() {
return Jaxer.request.headers["User-Agent"]
});

/** Does the user agent have FirePHP installed?
*
* The official FirePHP library checks the version, etc., but this just
* checks if "FirePHP" is in the user agent string
**/
Console.prototype.__defineGetter__("hasFirePHP", function() {
return this.userAgent.match("FirePHP") !== null
});

Console.prototype.__noSuchMethod__ = function __noSuchMethod__(id,args){
this.f(this.levels.warn, ["console."+id+"() is not implemented"]);
};

Jaxer.console = new Console();

Log.trace("*** Jaxer.console.js attached");

})();
[/codeblock]
User avatar
davey
 
Posts: 710
Joined: Mon Jul 23, 2007 9:25 pm

Postby davey » Fri Nov 14, 2008 1:12 am

Nathan,

spoke with one of the server devs, and he said to open a ticket for the header length issue. It is a limit in the jaxer protocol, that we should be able to extend.

create the ticket at http://support.aptana.com/asap
User avatar
davey
 
Posts: 710
Joined: Mon Jul 23, 2007 9:25 pm

Postby nlsmith » Thu Nov 20, 2008 3:15 pm

This now works as a Jaxer extension: http://nlsmith.com/projects/console/

Nathan
User avatar
nlsmith
 
Posts: 82
Joined: Wed Jan 23, 2008 2:57 am
Location: Iowa City, IA

Postby davey » Fri Nov 21, 2008 6:42 am

Congrats.

very nice. You have now published the FIRST non-aptana jaxer extension, send me a PM and i'll see about getting you a Jaxer shirt to commemorate the event.

Great the you put support for the CONFIG object in there. nice!


cheers.
User avatar
davey
 
Posts: 710
Joined: Mon Jul 23, 2007 9:25 pm


Return to Building Frameworks and Extensions on Jaxer