Websockets and the vanishing scopes

I have been working on a new application in FW\1. This application has a messaging aspect to it that sends alerts to users. The alerts can be either on screen, email, or a websocket message. When I went to start adding the websocket parts I started running into strange errors.

First, I was getting the following error after sending a websocket message and trying to do a redirect in FW\1.

The code looks like this (just a couple relevant parts)

view plain print about
1getmessengerService().sendMessage(message: "Message goes here", type: "error", delivery: 2);
2variables.fw.redirect('home:main.default', "displayMessage");

Original exception in onRequest
Cannot lock session scope.
Cannot use cflock to lock the application or session shared scopes without these scopes using the cfapplication tag. To use the session scope, you must enable session management. Application and/or Session variables must also be enabled in the ColdFusion Administrator. (Lock)

This struck me as odd because on refresh, I still had a session and was still logged in. I checked the redirect code and removed the preserve item (2nd argument) and tried again. This time I received a different error:

File not found: /Users/dferguson/Documents/Development/nfmtools/common/config/handler.cfc/Users/dferguson/Documents/Development/nfmtools/common/config/handler.cfc

Now I was totally lost. The redirect was going to a very odd place. However, the odd place it was going to was to not as odd as it seems. It was the defined handler for the websocket channel. So, I then decided to take out the handler and see how that would affect it. I now received this error:

File not found: /Applications/ColdFusion10/cfusion/wwwroot/CFIDE/websocket/ChannelListener.cfc/Applications/ColdFusion10/cfusion/wwwroot/CFIDE/websocket/ChannelListener.cfc

This is the default system handler for websockets. So, now I am totally confused and resting my head on my desk... over and over. I can't seem to get around these errors. Also, if I take out the wspublish() code everything works just fine. I even went as far as to put an application.cfc file in the directory with the handler to see if that did anything. Well... it didn't.

At this point I went in and started using the line debugger. If you have never used this tool then you are missing out. Instead of putting dumps and traces in your code you can actually watch the path that CF it taking through the code and what vars are being set. You can also see what is being passed to functions. So, I added a few breakpoints and ran the debugger.

[More]

Websockets demo: Log Watcher

So, I really wanted to create a demo for websockets that was not the norm. I wanted to try something that was more of a "what could I really do with with this" demo. After bouncing around a few ideas I finally settled on this, Log Watcher.

How many times have you wished you could tail a log file on your remote ColdFusion server? How many times have you remoted into your server to tail a log? If you are anything like me (probably not but for arguments sake lets say you are) you have probably done it once this week. At the very least once in the past few weeks.

What is log tailing? Well, to sum up it is viewing a log file as new lines are written to the file. This can be accomplished on unix using "tail -f {log}" or using something like BareTail on windows.

Now, what if you didn't have to remote into the server to tail the log? This is where Log Watcher comes in. Log Watcher allows you to tail a log via a web browser. This low overhead app does the same thing as a server based tail but with a little more flare.

So, how does it work? Well, it is all made possible due to ColdFusion 10. The interface is a simple browser app using Bootstrap for the pretty (I don't do pretty). The real guts of the app is on the server. Here, threading is used and a couple java objects to do a constant read on a file. The data (rows) from the log file are pushed to the client using websockets.

To accomplish this there are a few things I had to overcome. First was dealing with thread management. Creating threads that are designed to run forever is not something you generally want to do in a browser based application. This turned out be one of the more challenging parts. But this became infinitely easier thanks to a new setting in ColdFusion 10 to set the timeout to none.

view plain print about
1<cfsetting requesttimeout="0">

Next was dealing with the display and that all logs are not created equal. Then there was the issue of the app crashing the ColdFusion server (more on this later). A lot of trial and error went into building a way to display the log rows effectively. Once I solved these issues I was off to the races.

The client portion of the app is very basic. The only coolness there is the tiny amount of code for the client to subscribe to a web socket. Now, thanks to how websockets were implemented this results to just one tag

view plain print about
1<cfwebsocket name="lrbase" onmessage="wsFunctions.mycbHandler" subscribeto="lrbase"/>

This one little tag brings in all the necessary bits to do websockets. All we have to put in is the name of the websocket (as defined in applicaiton.cfc), the message handler, and the name of the channel to auto subscribe to on load.

Then just a little javascript to process the incoming messages. Here I handle the incoming data and split it up so I can display the data in a table. Name spacing my javascript has become a little habit. I don't do it all the time, but I find it useful when I want to encapsulate some code.

view plain print about
1var wsFunctions = (function() {
2
3    var wsResponsProcessor = function(obj){ // handle responses from WS
4
5        if (obj.type == 'data'){
6            
7            $('#mainContent2').show();            
8            output = splitMessage(obj.data);            
9            if (rowOut == 1 && useTableFormat(currentLog)){
10                    $('#headerRow').html(output);
11            
12            } else {
13            
14                if (loadDirection == 1){
15                    $(document).scrollTop($(document).height());                         
16                    $('#contentTable').append('<tr><td>'+rowOut+'</td>'+output+'</tr>');
17                } else {
18                    $('#contentTable').prepend('<tr><td>'+rowOut+'</td>'+output+'</tr>');
19                }    
20            }
21            rowOut++;
22        }    
23     
24 }
25    
26    return {
27        
28        mycbHandler: function(aEvent){
29         wsResponsProcessor(aEvent);
30        }
31    };    
32})();

To sum up all this javascript. If the type is "data" it processes the message. It is possible for the message to be other types for example the response message when subscribing. The message is then passed of to the splitMessage function. This function handles splitting the message into columns if the log is formatted that way. Then depending on load direction the row is either appended or prepended to the display.

To get the ball rolling there is a click handler on the log list. This processed the selected log and makes a ajax call to the server.

The logLoader.cfm does a few things. First, it includes the kill.cfm file. This file runs and sets a server var that any currently running thread will see and cause the thread to terminate. We then sleep the call for 1.5 seconds to give any other thread a chance to terminate. We then create the thread name and call the thread component.

The thread component is where the "magic" happens. This is where the thread is created and starts reading the requested log file.

view plain print about
1<cffunction name="readLog" access="public">
2        <cfargument name="threadName" type="string" required="true" />
3        <cfargument name="log" type="string" required="true" />
4        <cfargument name="channel" type="string" required="true" />
5        
6        
7        <cfthread action="run" name="#arguments.threadName#" logfile="#arguments.log#" channel="#arguments.channel#">
8        
9            <cfset server.channels["#attributes.channel#"] = thread.name>
10            <cfsetting requesttimeout="0">
11            <cfscript>                
12                FileName = "#server.logbase#/#attributes.logfile#.log";
13                FileIOClass = createObject("java", "java.io.FileReader");
14                FileIO = FileIOClass.init(FileName);                
15                LineIOClass = createObject("java", "java.io.BufferedReader");
16                LineIO = LineIOClass.init(FileIO);
17            
</cfscript>
18            <cfloop condition="1 eq 1">
19                <cfif server.channels["#attributes.channel#"] is not thread.name>
20                    <cfexit>
21                </cfif>        
22                
23                <cfset CurrLine = LineIO.readLine()>
24                
25                <cfif IsDefined("CurrLine")>                    
26                    <!---send websocket message--->
27                    <cfset wspublish("lrbase.#attributes.channel#", CurrLine)>
28                <cfelse>        
29                    <cfthread action="sleep" duration="1000" / >
30                </cfif>
31                    
32            </cfloop>
33        </cfthread>
34    </cffunction>

Walking through the function the first thing is does is create a thread with an action of run. This will create a running thread that will be come "detached" from the request that created it. First a server var is set with this threads name and then the timeout is set to 0 (infinity). Next a couple java objects are utilized to read the file. This will allow for a non-blocking read on the file. This type if read is desired so that the system can still write to the file while it is being read.

Next a loop is created that has a condition that will never end. Then in the loop a check is made to see if the server var for the active thread matches the current thread. If not the loop will exit thus also ending the thread. Next a line in the file is read. This will always read the next unread line. So, it will start at line 1 and get the next line on every loop. Eventually the currline var will not be defined. This is because the whole file is read. If the var is defined a websocket message is published to the channel for the app. This will magically make it back to the client and displayed.

Now, if the currline var is undefined the thread is put to sleep for a second. This is here because this is where the ColdFusion server was crashing. Not putting the sleep here caused CF to spike the processor up to 100% and eventually die. After some trial and error the 1 second sleep seemed to be the sweet spot. Now the thread was never over 2% cpu load.

As the loop continues over time any new line written to the file will be picked up and processed.

So there you have it. You can download the code (attached to this post) and check it out. Just unzip the code and drop it into your web root for ColdFusion 10.

Till next time...

--Dave

ColdFusion 10 WebSocket Demo App

So,

Websockets what are they? Here is what wikipedia had to say on the subject http://en.wikipedia.org/wiki/WebSocket. Basically put, websockets let you do a server "push" to a client browser.

If you have ever used BlazeDs you will be familiar with this type of technology. However with BlazeDS there was a constant open connection to the server. With websockets there is no persistant open connection. This greatly reduces overall overhead for both the client and the server.

So, what can you build with websockets? Well lots of stuff, you can create the obligatory chat client. Maybe an ad system that feeds new ads to a client. Maybe even a game where 2 people can play against each other.

Well, I figured it would be better just to build something to demo its features then to just talk about it. So, with that, I made a game. It is a 2 player game that allows people to play against each other in the same fashion as Battleship.

The downloadable code is attached to this post. A couple notes about the code. To run it effectively you need to open 2 browsers, not to browser windows. Also, I didn't to an exhaustive amount of testing on the app. If you just use it as intended it should work just fine. Also, if you have a console (firebug, or Developer Tools) running you can see the messages passed back and forth. In the base.js file at the very bottom uncomment the console.log line to see the output. Take a look at the code. Use it as an example to build your own next great thing. Let me know if you have any issues, questions or comments. I will do my best to assist as much as possible.

Till Next time...

--Dave