Adapting to the environment

Avatar
OL Learn Data

We all know that sound development principles tell us we should develop on one machine, test on a different one that’s an exact replica of the production server, and finally, implement the solution on that production server. Today we’ll look at how this can be done without having to customize the solution for each server.

The setup

A common hurdle with a DTAP architecture is how to apply the same solution across all PCs without having to make local changes to any of them (other than storing the configuration files on them, obviously). For instance, your solutions folder might be on the C: drive on your development system but on the D: drive on the production system. Or your connection string to a database might point to a local instance on your development machine, while the production system connects to a network database.

Having this kind of setup forces you to make changes to your solution when you implement it on any of the systems, which wastes time and introduces an element of risk. What if you forget to change that C: drive somewhere in the configuration?

Therefore, what you need is a solution that adapts to the environment on which it is running, without having to make changes to it each time it gets updated.

Building an adaptative solution

Making sure your solution adapts to any given environment is actually fairly simple, but it does require that you plan for it in the early stages of development. Doing it as an afterthought can be cumbersome, and is error-prone as well.

You first need to identify which elements in each system can vary. In this article, we’ll assume that there are only a few items that are different on each system, but you can apply the exact same techniques for any number of variable elements.

Let’s say we’ve identified the following elements as variable in an environment where we have 3 PCs (Dev, Test and Prod):

  1. The folder location for your resources
  2. The ip address of the Connect Server that will be handling all jobs.

Let’s put this in a JSON structure:

[ 
   {"name":"DEV",  "folder":"C:\resources\", "connect":"http://localhost:9340"},
   {"name":"TEST", "folder":"D:\resources\", "connect":"http://connect-dev.example.com:9340"},
   {"name":"PROD", "folder":"D:\resources\", "connect":"http://connect.example.com:9340"}
 ]

This type of structure is often called a manifest: it contains settings that are specific to your solution and to the systems it is running on. Save that structure in your resource folder, under the name manifest.json.

In Workflow, make sure you have 3 global variables that correspond to the values in the manifest: name, folder and connect (those are admittedly not the best names for global variables, but let’s keep them that way for the sake of simplicity)

Now in Workflow’s startup process for the solution, you have to implement a script that reads the manifest and sets the global variables to their proper value, depending on which server the solution is currently running on. But that raises the obvious question: how does the script know which system it is currently running on?

There are plenty of ways of identifying a server uniquely, but the easiest one is to stick with something that’s readily available in Windows as an environment parameter: the computer name. So let’s see how to obtain that value and use it to apply the proper settings from the manifest.

The script

The script first needs to get the computer name from the system. On all Windows system, this value is available via the %COMPUTERNAME% environment variable. To read it, use the following:

var computerName = getEnvVariable("COMPUTERNAME");

function getEnvVariable(vName){
   if(!/^\%.*\%$/g.test(vName)) vName = "%"+vName+"%";
   var shell =  new ActiveXObject( "WScript.Shell" ) 
   var result = shell.ExpandEnvironmentStrings(vName);
   if( result === vName ) result = "";
   return result
 } 

Here, we created a function to get the value of any environment variable, that way we can reuse it easily (that will come in handy later). The function does a little bit of validation on the parameter (you can pass either %COMPUTERNAME% or COMPUTERNAME) and then returns the result.

On any Windows computer, you can fetch the computer name by using the command set computername. So let’s imagine we ran that command on all three of our computers and received these values: DEV, TEST and PROD.

As you can see, those values happen to be the same ones we originally saved in our manifest. That’s the beauty of writing an article like this: I can make things happen exactly as I want! But obviously, in real life, you would adjust the manifest so that each name value corresponds to the actual computer name you retrieved from the environment variable.

At this stage, we naively believe we have everything we need to complete our script, which would now look like this:

// SECTION 1
var computerName = getEnvVariable("COMPUTERNAME").toUpperCase();
var fso = new ActiveXObject("Scripting.FileSystemObject");
var mFile = fso.openTextFile("C:\resources\manifest.json");
var manifest = JSON.parse(mFile.ReadAll());

// SECTION 2
 var thisComputer = manifest.find(function(obj){
  return obj.name.toUpperCase() == computerName;
});

// SECTION 3
Watch.SetVariable("global.name",thisComputer.name);
Watch.SetVariable("global.folder",thisComputer.folder);
Watch.SetVariable("global.connect",thisComputer.connect);

// SECTION 1
function getEnvVariable(vName){
  if(!/^\%.*\%$/g.test(vName)) vName = "%"+vName+"%";
  var shell =  new ActiveXObject( "WScript.Shell" )
  var result = shell.ExpandEnvironmentStrings(vName);
  if( result === vName ) result = "";
  return result
}

In SECTION 1, the script fetches the computer name and loads the manifest file into an array. SECTION 2 retrieves the array object that corresponds to the current computer. SECTION 3 sets our global variables according to the values in that object, and SECTION 4 is our utility function for retrieving the computer name.

But there are a couple of issues with this script. One of them is glaring, the other one is more subtle. Fortunately for us, they are both located on the same line, in SECTION 1:

var mFile = fso.openTextFile("C:\resources\manifest.json");

This line reads the manifest file from our resource folder… but the folder name is hardcoded, when the whole point of having a manifest is to never have to hard code anything! So we need to be able to read the manifest without specifying where to load the file from. Again, there are various methods for resolving that kind of thing, but let’s stick with what we now know we can access easily: environment variables. We simply have to make sure that on all 3 PCs, a common environment variable contains the path in which the manifest file is stored. This only needs to be done once on each computer, through the system properties of the computer:

Now that we have this new environment variable, we can use our JavaScript function to read its value and use the result to open the manifest file:

// SECTION 1
var computerName = getEnvVariable("COMPUTERNAME").toUpperCase();
var folder = getEnvVariable("OLConnectResources");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var mFile = fso.openTextFile(folder+"manifest.json");
var manifest = JSON.parse(mFile.ReadAll());

Obviously, since we now fetch the folder location from an environment variable, we no longer need to include that value in the manifest itself, so we can now remove it for all three computers and we can simply store the value of that environment variable in our global.folder variable.

So since reading environment variables is so simple, why can’t we use them for all our system-specific values? Well, we could… but when you want to change the values, you have to log on to each machine and change the environment variables on each of them, which can be cumbersome. You should only use a strict minimum of environment variables and then store all the rest of your settings in a manifest file.

But wait…

Yes, I know I said there were two issues with our line of code, and I only resolved one so far. That’s because the second issue is not specifically related to the subject of this article, it’s just a general consideration for all scripts written in Workflow.

The line:

var mFile = fso.openTextFile(folder+"manifest.json");

opens the file using the current system encoding. That means if you have accented characters in there, or any other characters that do not belong to the system’s code page, you might get some … ehhh … funky results. Whenever a script reads a file, it should use methods that allow it to specify utf-8 encoding, which happens to be the default encoding for JSON files. There is fortunately a relatively easy way to do this, by using the ADODB.Stream COM object instead of Scripting.FileSystemObject:

var myFileContent = readBinaryFile(folder+"manifest.json");

function readBinaryFile(filename) {
  var stream = new ActiveXObject("ADODB.Stream");
  var content;
  try {
    stream.Type = 2;
    stream.CharSet = "utf-8";
    stream.Open();
    stream.LoadFromFile(filename);
    content = stream.ReadText();
  } finally {
    stream.Close();
  }
  return content;
 }

I will be writing more about this specific topic in a future article, but in the meantime, feel free to use this piece of code whenever you want to read any kind of text file from a script.

Tagged in: DTAP, environment, manifest



Leave a Reply

Your email address will not be published. Required fields are marked *