ColdFusion 10 Backport: CallStackGet & CallStackDump

Next up on the hit list of backported functions from ColdFusion 10 to at least CF8+: CallStackGet & CallStackDump. These functions are aimed at grabbing a ColdFusion stack trace based on where they’re used, for more detailed information take a quick look through the linked manual entries.

Tracing

First and more important thing we need to work out, is how to get a stack trace. There are a couple of ways to achieve this:

  • Throwing and catching an exception and using the stack trace contained in the exception (Ben Nadel shows how).
  • Using the debugging service, although I think this may require debugging to be enabled (Yet again, Ben knows how).
  • Using java.lang.Thread and its getStackTrace().
  • Or java.lang.Throwable.getStackTrace().

I’ve picked the last option, not just because this blog post would be cut short or purely consist of reposts of Ben Nadel’s code (reformated with DeBenification). But because it’s probably the fastest, plus in some cases it might be what the other approaches are using at a lower level. The advantage of java.lang.Throwable to java.lang.Thread is it requires less steps (you don’t have to get the current thread first) and it has been around since Java 1.4.2 (CF7) making it a little more portable for legacy systems.

<!--- Works in Java 1.4.2+ and CF7+ --->
<cfdump var="#CreateObject("java", "java.lang.Throwable").getStackTrace()#" />

<!---
  Alternative using java.lang.Thread (Java 1.5+, CF8+)
  <cfdump var="#CreateObject("java", "java.lang.Thread").currentThread().getStackTrace()#" />
  <cfdump var="#thread#" />
--->

Information overload!

Unfortunately that stack trace doesn’t just return ColdFusion information, but all the Java stuff going on below it. We need to filter out everything apart from ColdFusion related entries, in order the match the CallStack* functions. Initially my thought was to just filter by file extensions (.cfm, .cfc) but I remembered that CFInclude will let you process any file extension as CFML. I noticed that normal pages executed with the runPage method name and functions / methods returned the method name runFunction. These felt safer and during testing I didn’t see those methods being used elsewhere in the trace, although if you do stumble across something then let me know :)

<cfscript>
  trace = CreateObject("java", "java.lang.Throwable").getStackTrace();
  cfTrace = ArrayNew(1);
  tCount = ArrayLen(trace);
  for (i = 1; i Lte tCount; i = i + 1) {
    if (ListFindNoCase('runPage,runFunction', trace[i].getMethodName())) {
      // Should be a CF element of the trace
      ArrayAppend(cfTrace, trace[i]);
    }
  }
</cfscript>
<cfdump var="#cfTrace#" />

Fantastic… but it’d be much nicer to have an array of structures just like CallStackGet would return, instead of those java.lang.StackTraceElement.

<cfscript>
  trace = CreateObject("java", "java.lang.Throwable").getStackTrace();
  cfTrace = ArrayNew(1);
  tCount = ArrayLen(trace);
  for (i = 1; i Lte tCount; i = i + 1) {
    if (ListFindNoCase('runPage,runFunction', trace[i].getMethodName())) {
      // Should be a CF element of the trace
      info = StructNew();
      info["Template"] = trace[i].getFileName();
      if (trace[i].getMethodName() Eq "runFunction") {
        info["Function"] = ReReplace(trace[i].getClassName(), "^.+\$func", "");
      } else {
        info["Function"] = "";
      }
      info["LineNumber"] = trace[i].getLineNumber();
      ArrayAppend(cfTrace, Duplicate(trace[i]));
    }
  }
</cfscript>
<cfdump var="#cfTrace#" />

From the above code you should be seeing an array containing a structure per CFML stack trace element. Containing the template, line number and function (if applicable). Exactly what CallStackGet is returning in ColdFusion 10. All that’s left is to wrap it into a function.

The function

<cffunction name="CallStackGet" output="false" returntype="array">
  <cfscript>
    var lc = StructNew();
    lc.trace = CreateObject("java", "java.lang.Throwable").getStackTrace();
    lc.op = ArrayNew(1);
    lc.elCount = ArrayLen(lc.trace);
    for (lc.i = 1; lc.i Lte lc.elCount; lc.i = lc.i + 1) {
      if (ListFindNoCase('runPage,runFunction', lc.trace[lc.i].getMethodName())) {
        lc.info = StructNew();
        lc.info["Template"] = lc.trace[lc.i].getFileName();
        if (lc.trace[lc.i].getMethodName() Eq "runFunction") {
          lc.info["Function"] = ReReplace(lc.trace[lc.i].getClassName(), "^.+\$func", "");
        } else {
          lc.info["Function"] = "";
        }
        lc.info["LineNumber"] = lc.trace[lc.i].getLineNumber();
        ArrayAppend(lc.op, Duplicate(lc.info));
      }
    }
    // Remove the entry for this function
    ArrayDeleteAt(lc.op, 1);
    return lc.op;
  </cfscript>
</cffunction>

The one part I’d like to point out is the ArrayDeleteAt(lc.op, 1) at the bottom of the function. This is needed to fully mimic the function from CF10, otherwise it would return information on itself at the top of every trace.

What about CallStackDump?

I haven’t forgotten and half the work is already being done by our new CallStackGet. What’s special about CallStackDump are the desintation options: console, browser and a file. Apart from that we just need to simply parse our array of structure into a string.

<cfscript>
  trace = CallStackGet();
  cftrace = ArrayNew(1);
  tCount = ArrayLen(trace);
  for (i = 1; i lte tCount; i = i + 1) {
    if (Len(trace[i]["Function"]) Gt 0) {
      ArrayAppend(cftrace, trace[i].Template & ":" & trace[i]["Function"] & ":" & trace[i].LineNumber);
    } else {
      ArrayAppend(cftrace, trace[i].Template & ":" & trace[i].LineNumber);
    }
  }
  cftrace = ArrayToList(cftrace, Chr(10));
  WriteOutput("<pre>" & cftrace & "</pre>");
</cfscript>

Finally, we need to handle the different outputs. The console destination is possible via java.lang.System.out and its printLn method. Thanks to (What? Again?) Ben Nadel and his massive exploration of GetPageContext, there’s a handy way to write output even when the function is set to output="false", cfsilent is used or other methods of suppressed output. With the file output, I simply just try and write to the path provided.

<cfscript>
  // Continued after previous snippet with "cftrace"
  destination = "console";
  if (destination Eq "browser") {
    GetPageContext().getCFOutput().print(cftrace);
  } else if (destination Eq "console") {
    CreateObject("java", "java.lang.System").out.println(cftrace);
  } else {
    fp = FileOpen(destination, "append");
    FileWrite(fp, cftrace & Chr(10));
    FileClose(fp);
  }
</cfscript>

The function

<cffunction name="CallStackDump" output="false" returntype="void">
  <cfargument name="destination" required="false" type="string" default="browser" />
  <cfscript>
    var lc = StructNew();
    lc.trace = CallStackGet();
    lc.op = ArrayNew(1);
    lc.elCount = ArrayLen(lc.trace);
    // Skip 1 (CallStackDump)
    for (lc.i = 2; lc.i lte lc.elCount; lc.i = lc.i + 1) {
      if (Len(lc.trace[lc.i]["Function"]) Gt 0) {
        ArrayAppend(lc.op, lc.trace[lc.i].Template & ":" & lc.trace[lc.i]["Function"] & ":" & lc.trace[lc.i].LineNumber);
      } else {
        ArrayAppend(lc.op, lc.trace[lc.i].Template & ":" & lc.trace[lc.i].LineNumber);
      }
    }
    lc.op = ArrayToList(lc.op, Chr(10));

    if (arguments.destination Eq "browser") {
      // Use the buffer since output = false
      GetPageContext().getCFOutput().print(lc.op);
    } else if (arguments.destination Eq "console") {
      CreateObject("java", "java.lang.System").out.println(lc.op);
    } else {
      lc.fp = FileOpen(arguments.destination, "append");
      FileWrite(lc.fp, lc.op & Chr(10));
      FileClose(lc.fp);
    }
  </cfscript>
</cffunction>

CFBackport on Github

Mentioning as always, these are part of my CFBackports project on Github. Feel free to log any issues or deviations from CF10 behaviour on there, or even fork the project and add your own backports. https://github.com/misterdai/cfbackport


Comments


David Boyer
David Boyer

Full-stack web developer from Cardiff, Wales. With a love for JavaScript, especially from within Node.js.

More...