Monday, February 16, 2009

Showing the progress of buildscripts in the dashboard.

For my buildscripts I hardly use batch files. These are ok for the short and quick stuff, but when it gets more complex, or takes a while to execute, I write a .Net Console program. The reasons :
  • string handling is a lot easier in .Net
  • you can easily obscure passwords in a .Net console program
  • if it needs be, you can call webservices
  • make the progress visible in the dashboard
  • ...
Calling external programs from .Net is very easy, you can do it with System.Diagnostics.Process. This gives very flexible control over the called program. Below is an example for calling Mage :
static void CallMage(string mageFolder, string mageArguments)
{
const Int32 TimeOutInSeconds = 20;

System.Diagnostics.Process MageProcess = new System.Diagnostics.Process();
string MageError = "";
string MageResult = "";

// setting up mage
MageProcess.StartInfo.UseShellExecute = false;
MageProcess.StartInfo.RedirectStandardError = true;
MageProcess.StartInfo.RedirectStandardOutput = true;
MageProcess.StartInfo.Arguments = mageArguments;
MageProcess.StartInfo.FileName = String.Format("{0}{1}{0}", "\"", mageFolder);

// running mage
MageProcess.Start();
MageProcess.WaitForExit(TimeOutInSeconds * 1000);

if (!MageProcess.HasExited)
{
MageProcess.Kill();
throw new Exception(String.Format("Mage has timed out after {0} seconds ", TimeOutInSeconds));
}

// checking result
MageError = MageProcess.StandardError.ReadToEnd();
MageResult = MageProcess.StandardOutput.ReadToEnd();

MageProcess.Close();

if (MageError.Length > 0)
{
throw new Exception(String.Format("Mage Error : \n {1}", MageError));
}

Console.WriteLine(" {0}", MageResult);
}


Now a more conrete example, below is a piece of the deployment script I use, I stripped a lot the functionality, but the core is the same. Also a short simple example shows more than a full blown one. Basically it unzips a file, and calls mage to resign an assembly. This call to mage is the procedure above.

static void Main(string[] args)
{
const string MageProgramLocation = @"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\mage.exe";

Int32 ReturnValue = 0;
ConsoleColor OriginalBackgroundColor = Console.BackgroundColor;

try
{
string tempFolder = @"d:\temp\unzip";

Console.WriteLine(" Un-Zipping to temp folder");
Ionic.Utils.Zip.ZipFile zip = new Ionic.Utils.Zip.ZipFile(@"d:\safe\programs\examples\1_0_1_256_FunkyStuff.zip");
zip.ExtractAll(tempFolder);
zip.Dispose();


string MageArguments;
MageArguments = String.Format(" -Sign {0}{1}{0} -CertFile {0}{2}{0} -Password {3}", "\"",
@"d:\temp\unzip\x.manifest",
@"d:\temp\keys\deployManifestKey",
"DeploymentKeyPWD");

CallMage(MageProgramLocation, "");

}
catch (Exception e)
{
Console.BackgroundColor = ConsoleColor.Red;
Console.WriteLine(e.ToString());
Console.BackgroundColor = OriginalBackgroundColor;
ReturnValue = 2;
}

Environment.ExitCode = ReturnValue;
}


Now the ones paying attention will notice that there is nothing special about the above code. There is nothing foreseen that will show any output to the dashboard of CCNet. And this is correct. However, the adjustment needed for this is very small. I made a small helper class that I use, just for this kind of things. The only thing I need to change to any console program is I have to make an instance of this class, and replace every Console.Writeline call to x.Writeline.
For example add static private CCNetExecListener CCNetListener = new CCNetExecListener(); just above static void Main, so it is known in the entire program, and replace every Console.Writeline call to CCNetListener.Writeline. This will make the above program to output the buildprogress when ran from CCNet, but it also remains usable when ran from the commandline. Nice and clean ;-)

Below is the code of the class.
using System;
using System.Collections.Generic;

public class CCNetExecListener
{
private string CCNetListenerFile = string.Empty;
private int AmountOfLinesToKeep = 10;
private Queue<string> Messages = new Queue<string>();

#region "Constructors"

public CCNetExecListener()
{
this.CCNetListenerFile = Environment.GetEnvironmentVariable("CCNetListenerFile");
}

public CCNetExecListener(int amountToKeep)
: this()
{
this.AmountOfLinesToKeep = amountToKeep;
}

#endregion

#region "Write Information"

public void WriteLine()
{
HandleNewInformation("");
}


public void WriteLine(bool value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(string value)
{
HandleNewInformation(value);
}

public void WriteLine(char value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(decimal value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(double value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(float value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(int value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(uint value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(long value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(object value)
{
HandleNewInformation(value.ToString());
}

public void WriteLine(string value, params object[] arg)
{
string info = string.Format(value, arg);
HandleNewInformation(info);
}

#endregion

#region "Private functions"

private void HandleNewInformation(string value)
{

string Data = string.Format("<Item Time={0}{1}{0} Data={0}{2}{0} />", "\"", GetTimeStamp(), value);


if (this.Messages.Count >= this.AmountOfLinesToKeep)
{
this.Messages.Dequeue();
}

this.Messages.Enqueue(Data);


Console.WriteLine(value);

WriteQueueData();


}

private void WriteQueueData()
{

if (this.CCNetListenerFile == string.Empty) return;

System.IO.StreamWriter TraceFile = default(System.IO.StreamWriter);

try
{
TraceFile = new System.IO.StreamWriter(this.CCNetListenerFile, false);
}
catch (Exception)
{
return;
}

TraceFile.AutoFlush = true;

TraceFile.WriteLine("<data>");

foreach (string s in this.Messages)
{
TraceFile.WriteLine(s);
}

TraceFile.WriteLine("</data>");
TraceFile.Close();

}

private string GetTimeStamp()
{
return System.DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
}

#endregion
}

No comments:

Post a Comment