Monitoring TeamBuild with CCTray, A Simple Solution

Most folks doing Continuous Integration in a .NET shop use CruiseControl.NET (at least in my experience). CruiseControl.NET comes with a little tray app, called CCTray, that allows you to monitor numerous builds you are interested in keeping an eye on.

Since moving some of our builds to Team Build and enabling Continuous Integration with TeamCI by Notion Solutions, I spiked out a project and threw it up on CodePlex, called TeamTray which is effectivly a replacement for CCTray that monitors build results from TFS.

Then the other day I was looking at how you could wire up CCTray to monitor builds. Some team members and I had discussed implementing a Remoting interface so that CCTray thought it was talking to a CruiseControl service and just within that service make the necessary calls to figure out the lastest build status of the different builds in a Team Project on TFS.

I started to dig into that and while it seems entirely possible and in many cases more useful (being able to send messages back to the server to do things like Force Builds), I wanted something I could get up an running in an hour or two.

My solution was to implement a simple web page that returned an XML structure that CCTray expects and use the third option on the "Add a Server" configuration dialog box.

In fact, I noticed the link on that dialog box that pointed me to a spec they had published (basically an XSD and description of the different parameters).

So, I set about building a simple one page ASP.NET project that on the Render override for the page, I set the ContentType = "text/xml" and then sent back the XML the tray app was expecting.

protected override void Render(HtmlTextWriter writer)
{
    Response.Clear();
    Response.ContentType = "text/xml";

XmlSerializer serializer = new XmlSerializer(typeof(Projects));
    serializer.Serialize(writer, GetProjects());
}

Projects GetProjects()
{        
    Shared.ConnectServer(System.Configuration.ConfigurationManager.AppSettings["tfsserver"]);
    List ccnetProjects = new List();

Microsoft.TeamFoundation.Server.ProjectInfo[] projects = Shared.GetProjects();
    foreach (Microsoft.TeamFoundation.Server.ProjectInfo project in projects)
    {
        List buildTypes = Shared.GetBuildTypes(project.Name);
        foreach (string buildType in buildTypes)
        {
            ProjectBuildType projBuildType = new ProjectBuildType(project.Name, buildType);
            ccnetProjects.Add(projBuildType.GetCCNetProject());
        }
    }

Projects p = new Projects();
    p.Project = ccnetProjects.ToArray();
    return p;
}

In order to make the serialization easier, I imported the published XSD into a new XSD file that I added to the project. I then used the xsd.exe command line utility that is part of the .NET SDK to generate the proxy classes that allow me to deal with objects instead of XML strings. I added that code file to the project.

I then wrote a method that would get connect to a configured server, get the projects, get the build types for those projects, loop through and get the latest status for each of those build types and add them to a collection, returning this to be serialized and spit out as content to be consumed by the CCTray app.

I ended up extracting some of the method out into a class called ProjectBuildType that took care of some of the details of getting the latest build status, etc.

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;



using Microsoft.TeamFoundation.Build.Proxy;




public class ProjectBuildType
{
    string _project;
    string _buildType;





public ProjectBuildType(string project, string buildType)
{
    _project = project;
    _buildType = buildType;
}

public string Project
{
    get { return _project; }
}

public string BuildType
{
    get { return _buildType; }
}

public BuildData GetLatestBuild()
{
    BuildData[] data = Shared.BuildStore.GetListOfBuilds(Project, BuildType);
    return data[data.Length - 1];
}

public ProjectsProjectLastBuildStatus GetPreviousBuildStatus()
{
    BuildData[] data = Shared.BuildStore.GetListOfBuilds(Project, BuildType);

    switch (data[data.Length - 2].BuildStatusId)
    {
        case 100:  // Success
            return ProjectsProjectLastBuildStatus.Success;
        case 200: // Failed
            return ProjectsProjectLastBuildStatus.Failure;
        default:   // Unknown
            return ProjectsProjectLastBuildStatus.Unknown;
    }
}

public override string ToString()
{
    return string.Format("{0} - {1}", Project, BuildType);
}

public ProjectsProject GetCCNetProject()
{
    BuildData[] data = Shared.BuildStore.GetListOfBuilds(Project, BuildType);

    ProjectsProject ccnetProject = new ProjectsProject();
    ccnetProject.name = this.ToString();
    ccnetProject.lastBuildLabel = data[data.Length - 1].BuildNumber;
    ccnetProject.lastBuildTime = data[data.Length - 1].FinishTime;
    ccnetProject.webUrl = data[data.Length - 1].BuildUri;

    switch (data[data.Length - 1].BuildStatusId)
    {
        case 100:  // Success
            ccnetProject.lastBuildStatus = ProjectsProjectLastBuildStatus.Success;
            ccnetProject.activity = ProjectsProjectActivity.Sleeping;
            break;
        case 200: // Failed
            ccnetProject.lastBuildStatus = ProjectsProjectLastBuildStatus.Failure;
            ccnetProject.activity = ProjectsProjectActivity.Sleeping;
            break;
        case 300:   // Stopped
            ccnetProject.lastBuildStatus = ProjectsProjectLastBuildStatus.Unknown;
            ccnetProject.activity = ProjectsProjectActivity.Sleeping;
            break;
        default:
            if (data.Length > 2)
            {
                ccnetProject.lastBuildLabel = data[data.Length - 2].BuildNumber;
                ccnetProject.lastBuildTime = data[data.Length - 2].FinishTime;
                ccnetProject.webUrl = data[data.Length - 2].BuildUri;
                ccnetProject.lastBuildStatus = GetPreviousBuildStatus();
            }
            else
            {
                ccnetProject.lastBuildLabel = string.Empty;
                ccnetProject.lastBuildTime = DateTime.MinValue;
                ccnetProject.webUrl = string.Empty;
                ccnetProject.lastBuildStatus = ProjectsProjectLastBuildStatus.Unknown;
            }

            ccnetProject.activity = ProjectsProjectActivity.Building;
            break;
    }

    return ccnetProject;
}

}

I ended up with being able to use the CCTray to monitor both legacy builds on CruiseControl.NET/NAnt as well as the new TeamBuild/MSBuild projects all within one view/tool.

The references to the "Shared" class above was taken from a the EventSubscriptionTool on CodePlex and has been been adapted and modifed per my needs, but the basic idea was to provide a wrapper around the TFS APIs:

// This class was taken from the EventSubscriptionTool (http://www.codeplex.com/tfseventsubscription)
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;




// TFS dependencies
using Microsoft.TeamFoundation.Proxy;
using Microsoft.TeamFoundation.Server;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.Build.Proxy;




public static class Shared
{
    static public TeamFoundationServer Server = null;
    static public ICommonStructureService CSSProxy = null;
    static public IEventService EventService = null;
    static public VersionControlServer VersionControl = null;
    static public BuildStore BuildStore = null;
    static public string ServerName;





public static void ConnectServer(string serverName)
{
        Shared.ServerName = serverName;

        Shared.Server = new TeamFoundationServer(serverName); 
        Server.Authenticate();
        Shared.EventService = (IEventService)Shared.Server.GetService(typeof(IEventService));
        Shared.CSSProxy = (ICommonStructureService)Shared.Server.GetService(typeof(ICommonStructureService));
        Shared.VersionControl = (VersionControlServer)Shared.Server.GetService(typeof(VersionControlServer));
        Shared.BuildStore = (BuildStore)Shared.Server.GetService(typeof(BuildStore));

}

public static ProjectInfo[] GetProjects()
{
    return Shared.CSSProxy.ListProjects();
}

// Adapted from http://notsosmartbuilder.blogspot.com/2006/11/how-to-get-build-types-list.html
public static List<string> GetBuildTypes(string project)
{
    List</string><string> buildTypes = new List</string><string>();

    ItemSet itemSet = Shared.VersionControl.GetItems(
         "$/" + project + "/TeamBuildTypes", 
         VersionSpec.Latest, 
         RecursionType.OneLevel, 
         DeletedState.NonDeleted, 
         ItemType.Folder);

    foreach (Item item in itemSet.Items)
    {
        if (Path.GetFileName(item.ServerItem) != "TeamBuildTypes")
        {
            buildTypes.Add(Path.GetFileName(item.ServerItem));
        }
    }

    return buildTypes;
}

}

Hope folks can find this useful.

I think a number of enhancements could and should be made to this. The biggest that I am thinking about is some sort of caching for requests that come in close together. If a team of 20 were all pinging this page at the same time every 5 seconds, there is a lot going on (in the API at least) to query the build server for latest status of a build. Possibly, caching the available projects and build types, or at least making that able to be tuned in the web.config would be useful and keep some potential pressure of this page and the resulting Team Foundation Server (not sure if that server is caching some of these queries, so it may not be necessary). Again, this is the result of about 2 hours of work (and about half of that researching the spec) so take it for what its worth.

Tags: cruisecontrol, team system, team build, msbuild, tfs, continuous integration