検索

記事
· 2016年3月15日 1m read

Code metrics over time

Among the outputs of our Yuzinji tool are two code metrics that it can be interesting to track over time as a development project proceeds. These are Size and XS. The first is fairly straightforward. As you write more code the size of your codebase increases. The XS metric (pronounced "excess") aims to quantify excessive structural complexity. XS is explained in some detail in this 2006 whitepaper from Headway Software, whose Structure101 toolset Yuzinji leverages.

By analyzing a codebase using Yuzinji at key milestones we can investigate how these metrics change. Here's a screenshot from a public repository we created from the classes in the SAMPLES namespace starting with Caché 2010.2 and reaching to recent 2016.1 and 2016.2 Field Test versions.

And here's a URL where you can browse this repository for yourself to discover more of what Yuzinji reveals.

http://demo.georgejames.com:8080/s101g/tracker/home.html

John Murray
George James Software

3 Comments
ディスカッション (3)1
続けるにはログインするか新規登録を行ってください
記事
· 2016年3月4日 1m read

2016.2 Field Test Kit 2016.2.0.605.0

I am pleased to announce the next 2016.2 field test kit, 2016.2.0.605.0.

There are about a hundred changes from the previous field test kit, including the following fixes to problems found by those using the kits in the field:

  • DLP3516, which fixes a problem in the Demo.Document.Data.Loader class
  • RJW2416, which fixes a problem with the telnet client disconnecting on Windows

One additional change of note: in this kit we have changed the on-disk format for DOCUMENT data.  If you want to preserve your data from an older instance and make it available in this version you should export the data from that older instance and import it into the new.  This is the method that you should always be using during the field test because, as you know, we do not support upgrades from one field test kit to another.

Please download the kit and give it a try; the latest field test of 2016.2 is available HERE.  And, as always, we welcome your feedback.

Steve Glassman, Director of QD

ディスカッション (0)0
続けるにはログインするか新規登録を行ってください
質問
· 2016年3月2日

What determines the value of $$$DefaultLanguage (used for localization) for a new Caché installation?

Is the default language (i.e., $$$DefaultLanguage, which is used as the basis for localization with $$$Text/etc. at compile time) always "en" for new Caché installations, or could it be different? How is this determined? I don't see an option to select a language during Caché installation.

Also, is there a supported/preferred API for setting the default language? Looking at %occMessages.inc, one option would be:

Set $$$DefaultLanguageNode = "en"

But I'd expect there to be a classmethod for this somewhere (and haven't managed to find it yet).

Background: My team is concerned about what might happen to the ^CacheMsg global if application code is compiled on other systems (possibly with different settings?) and messages are added as a result. I'm guessing there isn't a huge risk here, because compilation doesn't overwrite ^CacheMsg; the worst that could happen is that a "localized" value in the wrong language ends up in the global as an initial value. (I think this would still be a problem, in our case.)

UPDATE: My initial statement about compilation not overwriting ^CacheMsg is wrong. If a value is changed in the dictionary in the default language, compilation will fail with an error like:

 ERROR: test.MAC(3) : MPP5646 : ##expression failed with an error: $ze=<ZDUPL>macroText+48^%occMessages

This is because it looks the same as a hash collision.

3 Comments
ディスカッション (3)1
続けるにはログインするか新規登録を行ってください
この投稿は古いことに注意してください。
記事
· 2016年2月12日 8m read

Asynchronous Websockets -- a quick tutorial

Intro

 

Please note, this article is considered deprecated, check out the new revision over here: https://community.intersystems.com/post/tutorial-websockets

The goal of this post is to discuss working with Websockets in a Caché environment. We are going to have a quick discussion of what websockets are and then talk through an example chat application implemented on top of Websockets.

Requirements:

  • Caché 2016.1+
  • Ability to load/compile csp pages and classes

The scope of this document doesn't include an in-depth discussion of Websockets. To satisfy your thirst for details, please have a look at RFC6455[1]. Wikipedia also provides a nice high-level overview[2].

The quick'n'dirty version: Websockets provide a full-duplex channel over a TCP connection. This is mainly focused on, but not limited to, facilitating a persistent two-way communication channel between a web client and a webserver. This has a number of implications, both on the possiblities in application design as well as resource considerations on the server side.

The application

One of the standard examples, kind of the 'hello world' of Websockets is a chat application. You'll find numerous examples of similar implementations for many languages.

First we'll define a small set of goals for our implementetation. Some of these might sound basic, but keep in mind, any project lives and dies with a proper scope definition:

  • Users to send messages
  • Messages sent by one user should be broadcasted to all connected users
  • Provide different chat rooms
  • A user should get a list of currently connected users
  • A user should be able to set their own nickname

A production like chat application will have many more requirements, but the goal here is to demonstrate basic Websocket programming and not to replace IRC ;)

You'll find the code we are about to discuss in the repository (https://github.com/intersystems/websockets-tutorial). It contains:

  • ChatTest.csp -- The CSP page actually showing the chat
  • Chat.Server.cls -- Server side class managing the Websocket connection

To be able to manage the chatroom and the communication between the client and the server side, we are defining a couple of message formats we are going to use. Please note, that this is purely up to you. Websockets do not prescribe any format of the data being sent over it.

A chat message:

  {
         "Type":"Chat",
         "Message":"<msg>"
  }

A status update, informing the client of its websocketID

  {
         "Type":"Status",
         "WSID":"<websocketid>"
  }

The list of currently connected users (usually updated when a new user connects/disconnects):

  {
    "Type":"userlist",
    "Users": [ {"Name":"<username>"},
                            ....
                 ]
  }

The Client side

We are going to render the client side with a simple single html5 page. This is basic html with a little bit of css to make it look nice. For details, look at the implementation of ChatTest.csp itself. For simplicity we're pulling in jquery, which will allow us to make dynamic updates to the page a little easier.

ScreenShot

Down the road we are interested only in a couple of dom elements, identified by:

  • #chat -- the ul holding the chatmessages
  • #chatdiv -- the div holding #chat (used for automatic scrolling to the last message)
  • #userlist -- the ul holding the list of currently active users
  • #inputline -- the input field where the user is going to type messages

There are a couple of lines of javascript code in init() which are dealing with setting up events and handling input which we will not discuss. The first interesting bit is opening the WebSocket and binding function handlers to the relevant events:

In function init(): [..]

 ws = new WebSocket(((window.location.protocol == "https:") ? "wss:" : "ws:") + "//" + window.location.host + " #($system.CSP.GetDefaultApp($namespace))#/Chat.Server.cls"+"?room="+ROOM);

This opens a websocket connection to our server side class, either using ws or secured wss protocol, depending on the way we connected to our page.

  ws.onopen = function(event) {
     $("#headline").html("CHAT - connected");
  };

Once the WebSocket has been opened, we update the headline to let us know we are connected. Note that we're using the CSP expression "#($system.CSP.GetDefaultApp($namespace))#" to get the path of the current namespace. This will only work if you're using Caché to actually serve those pages. If you're only connecting to Caché as a backend and are planning on serving the page through a webserver directly, you'll have to hardcode the path to the websocket class (i.e. /csp/users/Chat.Server.cls)

  ws.onmessage = function(event) {
                  var d=JSON.parse(event.data);
                //var msg=d.Message;
                if (d.Type=="Chat") {
                        $("#chat").append(wrapmessage(d));
                        $("#chatdiv").animate({ scrollTop: $('#chatdiv').prop("scrollHeight")}, 1000);
                } else if(d.Type=="userlist") {
                        $("#userlist").html(wrapuser(data.Users));
                } else if(d.Type=="Status") {
                        document.getElementById("headline").innerHTML = "CHAT - connected - "+d.WSID;
                }
  };

This function handles an incoming message. We parse the JSON formatted data and then simply act based on the different types of messages as we've defined them earlier: * Chat: using the helper function wrapmessage, we add the new message to the #chat ul * userlist: using the helper function wrapuser, we generate and update the #userlist * Status: is a general status update, and we'll put the websocket ID into the headline

  ws.onerror = function(event) { alert("Received error"); };

This handles an error, we're simply displaying a message.

  ws.onclose = function(event) {
          ws = null;
          $("#headline").html("CHAT - disconnected");
  }

Closing the websocket leads to updating the headline again.

The one function left is

  function send() {
          var line=$("#inputline").val();
          if (line.substr(0,5)=="/nick"){
                  nickname=line.split(" ")[1];
                  if (nickname==""){
                          nickname="default";
                  }
          } else {
                  var msg=btoa(line);
                  var data={};
                  data.Message=msg;
                  data.Author=nickname;
             if (ws && msg!="") {
                       ws.send(JSON.stringify(data));
             }
          }
     $("#inputline").val("");
  }

Here we handle setting the nickname via "/nick newnickname" and sending a new chatmessage to the server. For that we b64 encode the textmessage and wrap it into an object, which we'll send json encoded to the server. We're doing the b64 encoding to avoid having to deal with special characters.

Server side.

The server side code is a simple class extending %CSP.WebSocket. We'll discuss the 5 functions in our implementation now.

Method OnPreServer() As %Status
{
  set ..SharedConnection=1
  set room=$GET(%request.Data("room",1),"default")
  set:room="" room="default"
  if (..WebSocketID'=""){
    set ^CacheTemp.Chat.WebSockets(..WebSocketID)=""
    set ^CacheTemp.Chat.Room(..WebSocketID)=room
  } else {
    set ^CacheTemp.Chat.Error($INCREMENT(^CacheTemp.Chat.Error),"no websocketid defined")=$HOROLOG 
  }

  Quit $$$OK
}

is the hook that is getting called for a new WebSocket connection. Here we set ..SharedConnection=1 to indicate that we want to be able to write to this socket from multiple processes. We are also recording the new socket ID and association with a chatroom into globals. For this example we're using ^CacheTemp.Chat.* in the hopes to not conflict with anything. Obviously these can be replace by other mechanisms.

Method Server() As %Status
{

        job ..StatusUpdate(..WebSocketID)
        for {                
        set data=..Read(.size,.sc,1) 
         if ($$$ISERR(sc)){
            if ($$$GETERRORCODE(sc)=$$$CSPWebSocketTimeout) {
                                  //$$$DEBUG("no data")
              }
              If ($$$GETERRORCODE(sc)=$$$CSPWebSocketClosed){
                      kill ^CacheTemp.Chat.WebSockets(..WebSocketID)
                      kill ^CacheTemp.Chat.Room(..WebSocketID)
                      do ..EndServer()        
                      Quit  // Client closed WebSocket
              }
         } else {
                 set mid=$I(^CacheTemp.Chat.Message)
                 set sc= ##class(%ZEN.Auxiliary.jsonProvider).%ConvertJSONToObject(data,"%Object",.msg)
                 set msg.Room=$G(^CacheTemp.Chat.Room(..WebSocketID))
                 set msg.WSID=..WebSocketID //meta data for the message
                 set ^CacheTemp.Chat.Message(mid)=msg.$toJSON()
                 job ..ProcessMessage(mid)
         }
        }

        Quit $$$OK
}

The Server() as %Status method is being called for a WebSocket afterwards. This holds our main loop for incoming messages from the client. An incoming message gets stored into a global after which we job off a updater (job ..ProcessMessage(mid)). This is all we're doing in our main loop.

/// clients for this room. 
ClassMethod ProcessMessage(mid As %String)
{
  set msg = ##class(%Object).$fromJSON($GET(^CacheTemp.Chat.Message(mid)))
  set msg.Type="Chat"

  set msg.Sent=$ZDATETIME($HOROLOG,3)
  set c=$ORDER(^CacheTemp.Chat.WebSockets(""))
  while (c'="") {
    set ws=..%New()
    set sc=ws.OpenServer(c)
    if $$$ISERR(sc){
      set ^CacheTemp.Chat.Error($INCREMENT(^CacheTemp.Chat.Error),"open failed for",c)=sc 
    }
    set sc=ws.Write(msg.$toJSON())
    set c=$ORDER(^CacheTemp.Chat.WebSockets(c))

  }
}

ProcessMessage is getting a message id passed in. It will parse the received json data into an %Object. We now $Order through our ^CacheTemp.Chat.WebSockets global to send the message to all connected chat clients for this room.

The BroadCast method demonstrates how to send a chatmessage to all connected clients. It's not being used on normal operations.

Wrapup

Exercise left for the user:

Implement the tracking of usernames on the server side and send a userlist message at the appropriate times.

Caveats, or why this isn't production code.

For a production ready system the inputs would need to be sanitized. We also hardly added any error trapping, access control, etc .

[1]https://tools.ietf.org/html/rfc6455 

[2]https://en.wikipedia.org/wiki/WebSocket

Feedback

Please feel free to provide feedback in the comments below. I'll also try and answer any questions!

28 Comments
ディスカッション (28)5
続けるにはログインするか新規登録を行ってください
質問
· 2016年2月11日

Ens.Rule.FunctionSet without parameter?

I'm writing some custom functions for use in a routing rule.  I have a few that are working, but right now I'm trying to use one that has no parameters.  Typically this would be a sub instead of a function, but I'm not familiar enough with Cache to know what I need to do here. 

Here is the code:

Class Custom.MHC.Common.CustomFunctions Extends Ens.Rule.FunctionSet
{

   /// Location and Revision of this file in Perforce (Auto-updating)
   Parameter SrcVer = "$Id$";

   /// Returns the current environment code,
   /// DEVELOPMENT, TEST, LIVE
   ClassMethod getEnvironment() As %String [ Final ]
   {
    ^%SYS("SystemMode")
   }
}

And here is what happens in the Rule Editor UI:

What should I do?  

6 Comments
ディスカッション (6)1
続けるにはログインするか新規登録を行ってください