Implementing a CCTray Transport Extension for Team Foundation Server - Part 1

Background

CCTray is a build monitoring application that is part of the CruiseControl.NET suite of build tools.  Originally, when our team began setting up builds within Team Foundation Server 2008 [TFS], we didn't have a build monitoring tool available to us.  A team member had used CCTray on previous projects and put together a web page that we could use to integrate the version 1.4 CCTray tool to monitor the status of our builds using the standard 'red-yellow-green' of continuous integration.  This worked fairly well, but was less than ideal for us, because it required that everyone point their monitoring clients to a server that didn't really belong to our main TFS installation.  So, I embarked upon the task shoehorning native TFS client access into CCTray to monitor the status of TFS builds directly.  This was fairly successful, and is still how the team monitors builds to this day, but unfortunately locks the team into using a fairly old custom build of the CCTray component to do the monitoring.

As I was investigating what it would take to upgrade the system to the new 1.5 version of CCTray, I stumbled upon this blog post by Craig Sutherland, a member of the CruiseControl.NET maintenance team.  In the blog post he described a method by which he added support for extensions to the base CCTray product.  His solution mirrored, in a more generic fashion, the approach I had taken, but had the added benefit of allowing the TFS integration component pieces I had hacked in to be self-contained in an extension DLL and deployed separately from the mainline CCTray executable.  This should allow simpler deployments and easier upgrades to newer versions of the tool for our team.

Sidebar: It should be mentioned at this point that the excellent Team Foundation Server Power Tools does contain a build monitoring application, but we found that it frequently 'lost track' of the builds and would refuse to update its state.  It also didn't give quite the same 'red-green-yellow' experience that we were looking for.

The Investigation

With the extension model in mind, I went out and grabbed the latest 1.5 RC1 release candidate build for CruiseControl.NET.  I was delighted to find that the transport extension pieces that Craig had outlined in his blog post had been [partially] integrated into the build, and proceeded to see what it would take to adapt my TFS integration code into an extension for the 1.5 CCTray client.

The UI for adding an extension based server transport is fairly straightfoward, first the 'Transport Extension' option is selected and the appropriate extension library is selected.  Once selected, you click the 'Configure Extension' button and an extension specific configuration dialog is presented.  Once configured, clicking OK attempts to contact the server through the extension and download a list of available build projects.

Add Build Server DialogAdd Build Server Dialog

Team Foundation Server stores pretty much everything, including build projects, by 'Team Project'.  In CCTray 1.4, I had the user include both the TFS Server name and team project as part of a 'URL' that CCTray would store for server connection.  This was not very user friendly, as the user needed to ensure correct spelling and make sure the right delimiters were included in the URL.  In this new version, I'd like to leverage the configuration dialog to select both the Team Foundation Server and the Team Project that should be associated with a given transport extension instance.

This was much more straightforward than I anticipated, as the Microsoft.TeamFoundation.Client namespace contains a publicly accessible class called RegisteredServers.  For now, the extension will just pull the current list of TFS servers registered for that user.  Getting Team Projects was similarly easy, as the TeamFoundationServer class allows us to retrieve an ICommonStructureService instance through the GetService() method.  The ICommonStructureService interface has a handy ListAllProjects() method which gives easy access to all the team projects that the current user can access on the selected server.

Armed with this knowledge, it was time to tackle the building of the TFS Transport Extension.

Implementing the Transport Extension

The ITransportExtension interface is fairly straightforward, it provides methods to retrieve an ICruiseServerManager that handles interactions with the server, and some other methods for surfacing configuration and providing detail about the extension.  The interface looks like this:

ITransportExtensionITransportExtension

In the previous version of my code, I had already implemented an ICruiseServerManager implementation, so that code came over pretty much 'out of the box' and I was able to focus on getting the server manager hooked into the appropriate server through the CCTray settings area.  This is where I stumbled upon the first problem.  It turns out that the transport extension implementation that's in the 1.5 RC1 codebase [and the current snapshot as well], does not enable you to actually configure the extension because the configuration button isn't hooked up.  Not only that, but the first thing that happens when clicking 'OK' is that an internal variable in the form is accessed which is never initialized, so an exception is immediately thrown.  Strangely enough, the code that I originally downloaded from the SourceForge site seemed to have a mockup of some instantiation code in a paint event, but recent downloads no longer have that code either.  So, if you want to follow along you'll need to download the patch from the above link and apply it to a local copy of the CCTray 1.5 RC1 source code.

Once the button was hooked up, the next task was to determine how [and when], the various properties that are exposed by the extension are assigned relative to the call to Configure(), which is the target for the custom configuration UI dialog.  It turns out, the flow looks something like this:

  1. Instantiate the Transport Extension implementation
  2. Call Configure() on the extension
  3. Construct a BuildServer instance using ITransportExtension.Configuration.Url and ITransportExtension.Settings property

The BuildServer instance created above is then used to assign the Configuration and Settings properties on future instantiations of the transport extension.  In essence, during Configuration, a 'blank' extension is populated with configuration data, then that configuration data is used to populate future instances of the extension for all build projects tied to a given build server.  The only potentially confusing bit is that during configuration, while the ONLY property in the Configuration class that matters is the Url, you MUST still populate the Configuration property during the call to Configure() or the transport extension cannot be used.

Storing Configuration Details

CCTray's ITransportExtension interface provides very simple string-based configuration settings storage, but each transport extension gets only a single string to store the entirety of its configuration.  In order to facilitate more detailed settings storage, it is necessary for the extension developer to decide how to map the single string of configuration data into some other form for consumption by the extension.  In the case of the TFS extension, I chose to store the configuration details in an in-memory class structure.  Currently the class contains only two properties, the Team Foundation Server hostname and the selected Team Project.  Eventually, I could see this being extended to support setting timeouts, pointing to source control, etc, but for now the server hostname and team project are sufficient.

The next task was to figure out how to provide for the serialization and deserialization of the settings data.  While an external configuration file would have been possible, and maybe desirable in certain scenarios, I wanted to avoid the complexity of another configuration file and decided to leverage the built-in XML Serialization capabilities of the .NET Framework to handle the configuration data.  I marked up the configuration class with the appropriate attributes from System.Xml.Serialization and added some static methods to the class to handle the serialization and deserialization duties.  The resulting class looks like this:

    [XmlRoot(ElementName="TFSServerManagerSettings")]
    public class TFSServerManagerSettings
    {
        #region Configuration Properties
        [XmlElement(ElementName="Server")]
        public string ServerName { get; set; }

        [XmlElement(ElementName = "Project")]
        public string TeamProject { get; set; }
        #endregion

        #region Serialization/Deserialization Support
        public static TFSServerManagerSettings GetSettings(string settingsString)
        {
            if (String.IsNullOrEmpty(settingsString))
            {
                return new TFSServerManagerSettings();
            }
            else
            {
                XmlSerializer ser = new XmlSerializer(typeof(TFSServerManagerSettings));
                using (StringReader rdr = new StringReader(settingsString))
                {
                    return ser.Deserialize(rdr) as TFSServerManagerSettings;
                }
            }
        }

        public override string ToString()
        {
            XmlSerializer ser = new XmlSerializer(typeof(TFSServerManagerSettings));
            StringBuilder sb = new StringBuilder();
            using (StringWriter writer = new StringWriter(sb))
            {
                ser.Serialize(writer, this);
            }

            return sb.ToString();
        }
        #endregion
    }

In this case I've chosen to override the ToString() method, whether or not this is the best solution is left up to the reader, but for this example it seemed to be the most 'natural' choice, given that the goal was to turn the class into a string.  Luckily in this case, this particular class is never thrown into list boxes or the like that would naturally call ToString() and expect some other representation than XML.

Next Steps

Now that we've defined our configuration data and have the UI linked together to provide the configuration bits, we can move into the meat of the extension, which is actually communicating with the Team Foundation Server and monitoring the builds.  We'll cover those pieces in Part 2.