Executing build tasks without a build server – Creating a custom ITask

Following on from the previous posts in this series, this post will look at how to create a custom task that can then be executed via BTE.

This post will provide an example of how to create a task that will allow execution of multiple tasks from a single command line execution. The task itself is fairly meaningless as you can simply invoke BTE several times to get the same result. It does however provide a good example of how to use some of the services provided by BTE.

The first thing to do is create a new library project in [VS]. You will then need to add BuildTaskExecutor.exe as a reference. You will also need to add a reference to System.ComponentModel.Composition.dll to get access to MEF.

image

The next step is to add a new class for the custom task. I have added a MultiTask class in this example and marked it as implementing the ITask interface. The class also needs to be decorated with the [Export(typeof(ITask))] attribute so that it can be picked up by MEF.

Next thing to do is add support for importing BTE types via MEF. This can either be done using either constructor or property injection. Constructor injection requires that the constructor be decorated with the [ImportingConstructor] attribute. Depending on the types being imported, constructor parameters may also need to define an Import attribute and possibly an import name. Property injection also requires the Import attribute and possibly an import name.

The MultiTask class will take the EventWriter service from BTE as a constructor import. It will also task the TaskExecutor service as a property import. The TaskExecutor cannot be imported in the constructor because it has a reference to TaskResolver which in turn as a reference too all loaded tasks. Having a task import TaskExecutor or TaskResolver in its constructor would cause a circular reference and MEF would throw a composition exception.

MultiTask defines the structure for the command line arguments so that the user can define multiple tasks and their arguments.

using System;
using System.Collections.Generic;
using System.Linq;
namespace Neovolve.BuildTaskExecutor.ThirdParty
{
    using System.ComponentModel.Composition;
    using System.Diagnostics;
    using System.Text;
    using System.Text.RegularExpressions;
    using Neovolve.BuildTaskExecutor.Extensibility;
    using Neovolve.BuildTaskExecutor.Services;

    [Export(typeof(ITask))]
    public class MultiTask : ITask
    {
        private Regex _taskExpression = new Regex("/task\\d+:(?<taskName>.+)", RegexOptions.Singleline);

        [ImportingConstructor]
        public MultiTask(EventWriter writer)
        {
            Writer = writer;
        }

        public Boolean Execute(IEnumerable<String> arguments)
        {
            List<String> taskArguments = null;

            foreach (String argument in arguments)
            {
                Match taskNameMatch = _taskExpression.Match(argument);

                if (taskNameMatch.Success)
                {
                    // This is a new task, execute any arguments already calculated
                    if (InvokeTask(taskArguments) == false)
                    {
                        return false;
                    }

                    // Parse out the task name
                    String taskName = taskNameMatch.Groups["taskName"].Value;

                    taskArguments = new List<String>
                                    {
                                        taskName
                                    };
                }
                else if (taskArguments != null)
                {
                    taskArguments.Add(argument);   
                }
            }
            
            return InvokeTask(taskArguments);
        }

        private Boolean InvokeTask(List<String> taskArguments)
        {
            if (taskArguments == null)
            {
                return true;
            }

            String message = "Invoking";

            taskArguments.ForEach(x => message += " " + x);

            Writer.WriteMessage(TraceEventType.Verbose, message);

            return Executor.Execute(taskArguments);
        }

        public Boolean IsValidArgumentSet(IEnumerable<String> arguments)
        {
            if (arguments.Any() == false)
            {
                Writer.WriteMessage(TraceEventType.Verbose, "No command line arguments provided");

                return false;
            }

            String firstTask = arguments.First();

            if (_taskExpression.IsMatch(firstTask) == false)
            {
                Writer.WriteMessage(TraceEventType.Verbose, "The first argument is not in the form '/taskN:' where N is a number.");

                return false;
            }

            return true;
        }

        public String CommandLineArgumentHelp
        {
            get
            {
                StringBuilder builder = new StringBuilder("/task1:<task1Name> [<task1Args>] [/task2:<task2Name> [<task2Args>]] ... [/taskN:<taskNName> [<taskNArgs>]]");

                builder.AppendLine();
                builder.AppendLine();
                builder.AppendLine("/taskN:<taskNName>\tThe task number to execute.");
                builder.AppendLine("\t\t\tN should be sequential in the command line.");
                builder.AppendLine("<taskNArgs>\t\tThe command line arguments for the associated task.");

                return builder.ToString();
            }
        }

        public String Description
        {
            get
            {
                return "Executes multiple tasks.";
            }
        }

        public IEnumerable<String> Names
        {
            get
            {
                return new[]
                       {
                           "MultiTask", "mt"
                       };
            }
        }

        [Import]
        private TaskExecutor Executor
        {
            get;
            set;
        }

        private EventWriter Writer
        {
            get;
            set;
        }
    }
}

The assembly that contains this custom task needs to reside in the same directory as BuildTaskExecutor.exe for MEF to resolve the task. Running BTE with the generic help task will prove that this custom task is being picked up by MEF.

image

We can then check that the task is able validate command line arguments correctly. We will turn on verbose event writing to see a bit more detail here.

image

You will notice here that the help task is rendering the command line help for the custom task given that the command line arguments have failed validation.

We can also provide an invalid argument to the task to test the other validation logic of the task.

image

We can now get the task to successfully execute multiple tasks. In this case the command is to output the help for the wov and tfsedit tasks.

image

One final action to check is that the second task is not invoked if the first task fails.

image

You can see here that creating a custom ITask implementation to be invoked by BTE is really easy.

It is now up to you to create your own tasks for any actions that you require in your own solutions. If you like, you could even contribute your tasks to the BTE project out on Codeplex.

Executing build tasks without a build server – Example scenario

My last few posts have described the process of building an application that can manage custom build tasks without the availability of a full TFS environment (here, here and here). This post will look at how I use this application and all its tasks to manage the product version of a [VS] solution. The product I originally wrote BuildTaskExecutor (BTE) for is the .Net port of my old VB6 Switch program which is now hosted out on Codeplex (here).

The build tasks I want to execute when this solution compiles are:

  • Increment the build number of the product version
    • Check out from TFS as required
  • Sync updated product version into wix project
    • Check out from TFS as required
  • Rename wix output to include the product version

The solution for Switch has several projects. These are:

  • Neovolve.Switch.Extensibility
  • Neovolve.Switch.Skinning
  • Neovolve.Switch
  • Neovolve.Switch.Deployment
  • Unit test projects

image

All the code projects link in a ProductInfo.cs file so that they all compile with the same product information.

image

The important configuration in this file with regard to versioning the product is the AssemblyVersionAttribute. Each project that links in this file will compile a binary with the same version information.

Increment the build number of the product version

The first task for the custom build actions is to increment the build number of the product before it compiles. This is done by invoking BTE in the pre-build event of the project that will compile first. You can look at the build order of the solution to easily figure out which project this will be.

image

The pre-build event of this project can then invoke BTE to increment the build number. BTE will attempt to check out the file from TFS before incrementing the build number as this action will make a change to the file.

The pre-build event for the Neovolve.Switch.Extensibility project has been changed to invoke the BTE TfsEdit task followed by the IncrementAssemblyVersion task.

If Not '$(ConfigurationName)' == 'Release' GOTO End

CD "$(SolutionDir)"
CALL bte tfsedit /pattern:"$(SolutionDir)Solution Items\ProductInfo.cs" /i
CALL bte iav /pattern:"$(SolutionDir)Solution Items\ProductInfo.cs" /b

GOTO End

:End

This script will only execute BTE when the Release build is selected. I didn’t want to increment the build number for Debug build configurations. When it is in a release build, the TfsEdit task has been configured here to ignore any failures to cater for scenarios where someone is building the solution without a TFS workspace mapping for the solution. The script then increments just the build number in the specified file.

Sync updated product version into wix project

The next step is to get the generation of the Wix project to use the same version information (which has now been incremented). This is unfortunately not as simple as linking in a common ProductInfo.cs file as it is with the C# projects. Wix projects define the version of the MSI in a version attribute on the Product element. This may also use a wix variable that may be declared in any file in the Wix project.

This is the scenario for the Neovolve.Switch.Deployment project. The beginning of the Product.wxs file defines the product version using a wix variable that is defined in an include file.

<?xml version="1.0"
      encoding="UTF-8" ?>
<?include Definitions.wxi ?>

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:netfx="http://schemas.microsoft.com/wix/NetFxExtension">
  <Product Id="*"
           Name="$(var.PRODUCTNAME)"
           Language="1033"
           Version="$(var.ProductVersion)"
           Manufacturer="$(var.COMPANYNAME)"
           UpgradeCode="0FD2155F-5676-448F-864A-482BE0C26E97">

The Neovolve.Switch.Deployment project will also define a pre-build event script like the one defined above for the Neovolve.Switch.Extensibility project.

CD "$(SolutionDir)"
CALL bte tfsedit /pattern:"$(ProjectDir)Definitions.wxi" /i
CALL bte swv /pattern:"$(ProjectPath)" /source:"$(SolutionDir)Neovolve.Switch\$(OutDir)Switch.exe" /M /m /b /r

The BTE TfsEdit task is used again here to attempt to check out the Definitions.wxi file from TFS as it is this file that contains the product version in a project variable. The BTE SyncWixVersion task then obtains the product version from the compiled Switch application and synchronises it into the Wix project before the MSI is compiled.

One important thing to note here is that the source of the version number is the compiled output of the Switch application (in the current solution build) rather than reading the version from a source file (ProductInfo.cs). This is important because the AssemblyVersionAttribute in ProductInfo.cs may only define 1.0.*, or 1.0.1.*. The full version information for wildcard version numbers is only available after the compiler has generated the application. In this case, the SyncWixVersion (or swv) task is taking all version parts  (/M = major, /m = minor, /b = build, /r = revision) from Switch.exe and pushing them into the wix product version value.

Rename wix output to include the product version

Lastly, I want the output of the Wix project to include the version number. This task could update the output file name in the project property prior to compiling the MSI. This would however be messy for two reasons:

  1. Changing the project during compilation will cause [VS] to want to reload the project. This is not really a problem but is a bad user experience as reload project dialogs will get in the way.
  2. The next time the project compiles, the task will have to deal with having a prior version number in the output name that it will need to parse out and replace. Easily achievable with a regular expression, but messy.

The simpler solution is to just rename the output of the project once compilation has completed.

A post-build event script is added to the Neovolve.Switch.Deployment project in order to resolve the output of the Wix project and rename it.

CD "$(SolutionDir)"
CALL bte wov /pattern:"$(ProjectPath)"

BTE invokes the WixOutputVersion task against the wix project. It will look at all the build configuration for the project and rename as many files as possible that match the output name of the project. It will use the product version information that is defined against the Wix project that was synchronised in the pre-build event script.

The result

When this solution is compiled, the following is seen in the build log (severely cut down for brevity).

------ Rebuild All started: Project: Neovolve.Switch.Extensibility, Configuration: Release Any CPU ------
  BuildTaskExecutor - Neovolve Build Task Executor 1.0.0.29135
  
  Executing task 'TfsCheckout'.
  Using 'C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\tf.exe' to check out files.
  Searching for matching files under 'D:\Codeplex\Neovolve.Switch\Solution Items\'.
  Checking out file 'D:\Codeplex\Neovolve.Switch\Solution Items\ProductInfo.cs'.
  Solution Items:
  ProductInfo.cs
  
  BuildTaskExecutor - Neovolve Build Task Executor 1.0.0.29135
  
  Executing task 'IncrementAssemblyVersion'.
  Searching for matching files under 'D:\Codeplex\Neovolve.Switch\Solution Items\'.
  Updating file 'D:\Codeplex\Neovolve.Switch\Solution Items\ProductInfo.cs' from version '2.0.2.0' to '2.0.3.0'.
  elapsed time: 134.0076ms
  elapsed time: 239.0136ms
  
  Microsoft (R) .NET Framework Strong Name Utility  Version 3.5.30729.1
  Copyright (c) Microsoft Corporation.  All rights reserved.
  
  Assembly 'obj\Release\Neovolve.Switch.Extensibility.dll' successfully re-signed
  Neovolve.Switch.Extensibility -> D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Extensibility\bin\Release\Neovolve.Switch.Extensibility.dll
------ Rebuild All started: Project: Neovolve.Switch.Skinning, Configuration: Release Any CPU ------

  Microsoft (R) .NET Framework Strong Name Utility  Version 3.5.30729.1
  Copyright (c) Microsoft Corporation.  All rights reserved.
  
  Assembly 'obj\Release\Neovolve.Switch.Skinning.dll' successfully re-signed
  Neovolve.Switch.Skinning -> D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Skinning\bin\Release\Neovolve.Switch.Skinning.dll
------ Rebuild All started: Project: Neovolve.Switch.Extensibility.UnitTests, Configuration: Release Any CPU ------
  
  Microsoft (R) .NET Framework Strong Name Utility  Version 3.5.30729.1
  Copyright (c) Microsoft Corporation.  All rights reserved.
  
  Assembly 'obj\Release\Neovolve.Switch.Extensibility.UnitTests.dll' successfully re-signed
  Neovolve.Switch.Extensibility.UnitTests -> D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Extensibility.UnitTests\bin\Release\Neovolve.Switch.Extensibility.UnitTests.dll
------ Rebuild All started: Project: Neovolve.Switch, Configuration: Release x86 ------
  
  Microsoft (R) .NET Framework Strong Name Utility  Version 3.5.30729.1
  Copyright (c) Microsoft Corporation.  All rights reserved.
  
  Assembly 'obj\x86\Release\Switch.exe' successfully re-signed
  Neovolve.Switch -> D:\Codeplex\Neovolve.Switch\Neovolve.Switch\bin\Release\Switch.exe
------ Rebuild All started: Project: Neovolve.Switch.UnitTests, Configuration: Release Any CPU ------
  Neovolve.Switch.UnitTests -> D:\Codeplex\Neovolve.Switch\Neovolve.Switch.UnitTests\bin\Release\Neovolve.Switch.UnitTests.dll
------ Rebuild All started: Project: Neovolve.Switch.Deployment, Configuration: Release x86 ------
        CD "D:\Codeplex\Neovolve.Switch\"
CALL bte tfsedit /pattern:"D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\Definitions.wxi" /i
CALL bte swv /pattern:"D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\Neovolve.Switch.Deployment.wixproj" /source:"D:\Codeplex\Neovolve.Switch\Neovolve.Switch\bin\Release\Switch.exe" /M /m /b /r
        BuildTaskExecutor - Neovolve Build Task Executor 1.0.0.29135
        Executing task 'TfsCheckout'.
        Using 'C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\tf.exe' to check out files.
        Searching for matching files under 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\'.
        Checking out file 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\Definitions.wxi'.
        Neovolve.Switch.Deployment:
        Definitions.wxi
        BuildTaskExecutor - Neovolve Build Task Executor 1.0.0.29135
        Executing task 'SyncWixVersion'.
        Searching for matching files under 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\'.
        Updating wix project 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\Neovolve.Switch.Deployment.wixproj' from version '2.0.2.0' to '2.0.3.0'.

        CD "D:\Codeplex\Neovolve.Switch\"
CALL bte wov /pattern:"D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\Neovolve.Switch.Deployment.wixproj"
        BuildTaskExecutor - Neovolve Build Task Executor 1.0.0.29135
        Executing task 'WixOutputVersion'.
        Searching for matching files under 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\'.
        Moving 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\bin\Release\Neovolve Switch.msi' to 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\bin\Release\Neovolve Switch 2.0.3.0.msi'.
        Moving 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\bin\Release\Neovolve Switch.wixpdb' to 'D:\Codeplex\Neovolve.Switch\Neovolve.Switch.Deployment\bin\Release\Neovolve Switch 2.0.3.0.wixpdb'.
========== Rebuild All: 6 succeeded, 0 failed, 0 skipped ==========

Build Summary
-------------
00:13.492 - Success - Release Any CPU - Neovolve.Switch.Extensibility\Neovolve.Switch.Extensibility.csproj
00:12.825 - Success - Release x86 - Neovolve.Switch\Neovolve.Switch.csproj
00:12.793 - Success - Release x86 - Neovolve.Switch.Deployment\Neovolve.Switch.Deployment.wixproj
00:07.458 - Success - Release Any CPU - Neovolve.Switch.Skinning\Neovolve.Switch.Skinning.csproj
00:07.067 - Success - Release Any CPU - Neovolve.Switch.Extensibility.UnitTests\Neovolve.Switch.Extensibility.UnitTests.csproj
00:01.879 - Success - Release Any CPU - Neovolve.Switch.UnitTests\Neovolve.Switch.UnitTests.csproj

Total build time: 00:55.555

========== Rebuild All: 6 succeeded or up-to-date, 0 failed, 0 skipped ==========

You can see from the logs that BTE has done the following throughout the build of the solution:

  • Checked out ProductInfo.cs from TFS
  • Identified the existing version as 2.0.2.0
  • Incremented version to 2.0.3.0
  • Synchronised the Wix project to 2.0.3.0
  • Moved the Wix project output to the same filename with the addition of the version number.

The solution now contains two files that are checked out. These are ProductInfo.cs and Definitions.wxi.

image

The target directory of the Neovolve.Switch.Deployment project now contains nicely renamed output.

image

Running all the BTE executions in the project build events is a batch file called bte.bat. This is located in the solution root and makes it easy for any project in the solution to invoke BTE with minimal noise in the build event script window. The batch file simply invokes BTE with all the command line parameters and manages the return code.

@ECHO OFF
References\Neovolve\BuildTaskExecutor\BuildTaskExecutor.exe %*

If errorlevel 1 GOTO BuildTaskFailed
GOTO BuildTaskSuccessful

:BuildTaskFailed
exit 1
:BuildTaskSuccessful

You have seen here how a flexible little tool like BTE can integrate into a solution build to achieve valuable results when you don’t have access to a TFS build infrastructure.

The next post will provide an example of how to create a custom task for BTE to pick up and execute.

Executing build tasks without a build server – In action

The previous post provided the implementation of the BTE application. It described how the implementation of BTE is able to satisfy the original design requirements by providing an extensible task execution model.

The following are some examples of BTE running on the command line. I am using a batch file in order to shorten BuildTaskExecutor.exe down to bte.

1) Executing BTE without arguments

image[8]

2) Executing the help task

image[11]

3) Executing the help task for a specific task name (or alias in this case)

image[14]

4) Executing with verbose logging

image[17]

The first version of BTE provides the following tasks built into the application (as defined by the help output):

BuildTaskExecutor BinaryOutputVersion|bov
Renames the project output to include the product version of the project output.

BuildTaskExecutor Help|/?
Displays help information about the available tasks

BuildTaskExecutor IncrementAssemblyVersion|iav
Increments version number parts in AssemblyVersionAttribute entries in code files

BuildTaskExecutor SyncWixVersion|swv
Synchronizes the product version in a Wix project to the version of a binary file.

BuildTaskExecutor TfsCheckout|tfsedit
Checks out files from TFS based on a search pattern. The checkout will use default credentials resolved for the identified files.

BuildTaskExecutor WixOutputVersion|wov
Renames the Wix project output to include the wix product version.

The next post will provide a scenario where these tasks are used to manage the version of a solution without a full TFS environment.

Executing build tasks without a build server – Implementation

My previous post outlines my design requirements for a set of build task actions. I want to execute these tasks for my personal projects that do not have the backing of a fully functional TFS deployment. This post will look at how to implement these requirements using a console application.

Neovolve.BuildTaskExecutor (or BTE) is the application that will execute specific tasks based on command line parameters. It implements MEF for the extensibility support so that additional tasks can be added at any time with zero impact on the application itself.

Extensibility

The main interface for BTE extensibility is the ITask interface. It provides the ability for BTE to identify the command line names associated with the task, validate command line arguments, obtain help information about the task and to execute the task.

namespace Neovolve.BuildTaskExecutor.Extensibility
{
    using System;
    using System.Collections.Generic;

    public interface ITask
    {
        Boolean Execute(IEnumerable<String> arguments);

        Boolean IsValidArgumentSet(IEnumerable<String> arguments);

        String CommandLineArgumentHelp
        {
            get;
        }

        String Description
        {
            get;
        }

        IEnumerable<String> Names
        {
            get;
        }
    }
}

The IEventWriter interface supports writing event messages. BTE provides an implementation of this interface that writes messages to the console. Custom implementations can be provided to output event messages to other locations.

namespace Neovolve.BuildTaskExecutor.Extensibility
{
    using System;
    using System.Diagnostics;

    public interface IEventWriter
    {
        void WriteMessage(TraceEventType eventType, String message, params Object[] arguments);
    }
}

The IVersionManager interface provides the ability to read and write version information from a file path. BTE provides three implementations of this class. They manage version information for C# AssemblyInfo.cs style files, Wix projects and binary files. These three implementations can be added to custom tasks by using name specified imports such as [Import(VersionManagerExport.Wix)] IVersionManager versionAction.

namespace Neovolve.BuildTaskExecutor.Extensibility
{
    using System;

    public interface IVersionManager
    {
        Version ReadVersion(String filePath);

        void WriteVersion(String filePath, Version newVersion);
    }
}

Services

BTE provides several service classes that execute tasks and provide additional service support to tasks.

The TaskExecutor and the TaskResolver classes are the core of the application. They identify which task to execute based on the first command line argument and then execute the resolved task with the remaining command line arguments.

These classes are listed below with their method bodies removed for brevity.

namespace Neovolve.BuildTaskExecutor.Services
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Diagnostics;
    using System.Linq;
    using Neovolve.BuildTaskExecutor.Extensibility;
    using Neovolve.BuildTaskExecutor.Properties;

    [Export]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class TaskResolver
    {
        [ImportingConstructor]
        public TaskResolver(EventWriter writer)
        {
        }

        public ITask ResolveTask(String taskName)
        {
        }

        [ImportMany]
        public IEnumerable<ITask> Tasks
        {
            get;
            private set;
        }

        private EventWriter Writer
        {
            get;
            set;
        }
    }
}
namespace Neovolve.BuildTaskExecutor.Services
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Diagnostics;
    using System.Linq;
    using Neovolve.BuildTaskExecutor.Extensibility;
    using Neovolve.BuildTaskExecutor.Properties;
    using Neovolve.BuildTaskExecutor.Tasks;

    [Export]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class TaskExecutor
    {
        [ImportingConstructor]
        public TaskExecutor(EventWriter writer)
        {
        }

        public Boolean Execute(IEnumerable<String> arguments)
        {
        }

        [Import]
        private TaskResolver Resolver
        {
            get;
            set;
        }

        private EventWriter Writer
        {
            get;
            set;
        }
    }
}

The EventWriter class is another service available in BTE. It wraps all the available IEventWriter implementations for an easy way to write event messages. It also manages the logic around the event writing level that can be configured on the command line.

namespace Neovolve.BuildTaskExecutor.Services
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Diagnostics;
    using System.Linq;
    using Neovolve.BuildTaskExecutor.Extensibility;

    [Export]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class EventWriter
    {
        [ImportingConstructor]
        public EventWriter([ImportMany] IEventWriter[] eventWriters, TraceEventType eventLevel)
        {
            if (eventWriters == null)
            {
                throw new ArgumentNullException("eventWriters");
            }

            EventWriters = eventWriters.ToList();
            EventLevel = eventLevel;
        }

        public void WriteMessage(TraceEventType eventType, String message, params Object[] arguments)
        {
            if (eventType > EventLevel)
            {
                return;
            }

            EventWriters.ForEach(x => x.WriteMessage(eventType, message, arguments));
        }

        public TraceEventType EventLevel
        {
            get;
            private set;
        }

        private List<IEventWriter> EventWriters
        {
            get;
            set;
        }
    }
}

Task Execution

Finally there is the Program class that is the entry point for BTE. It resolves the TaskExector from an internal ServiceManager and starts processing the command line arguments that are also resolved via MEF.

namespace Neovolve.BuildTaskExecutor
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using Neovolve.BuildTaskExecutor.Properties;
    using Neovolve.BuildTaskExecutor.Services;

    internal class Program
    {
        private static Int32 Main()
        {
            using (ServiceManager manager = new ServiceManager())
            {
                Lazy<EventWriter> writerService = manager.GetService<EventWriter>();
                Lazy<TaskExecutor> executorService = manager.GetService<TaskExecutor>();

                try
                {
                    EventWriter writer = writerService.Value;

                    if (writer == null)
                    {
                        throw new InvalidOperationException("Failed to resolve writer");
                    }

                    WriteApplicationInfo(writer);

                    TaskExecutor taskExecutor = executorService.Value;

                    if (taskExecutor == null)
                    {
                        throw new InvalidOperationException("Failed to resolve executor");
                    }

                    Lazy<IEnumerable<String>> arguments = manager.GetService<IEnumerable<String>>();

                    Boolean success = taskExecutor.Execute(arguments.Value);

                    if (success)
                    {
                        return 0;
                    }

                    return 1;
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);

                    return 1;
                }
                finally
                {
                    manager.ReleaseService(writerService);
                    manager.ReleaseService(executorService);

#if DEBUG
                    Console.ReadKey();
#endif
                }
            }
        }

        private static void WriteApplicationInfo(EventWriter writer)
        {
            String assemblyPath = typeof(Program).Assembly.Location;
            FileVersionInfo versionInfo = FileVersionInfo.GetVersionInfo(assemblyPath);

            writer.WriteMessage(TraceEventType.Information, Resources.Executor_ApplicationInformation, versionInfo.ProductVersion);
            writer.WriteMessage(TraceEventType.Information, String.Empty);
        }
    }
}

This post has outlined the core implementation of BTE. The core of BTE and the tasks already in the application satisfy all the design requirements from the previous post. The next post will look at how the application looks when it is invoked on the command line.

Executing build tasks without a build server – Design

At work we run TFS2010 with build controllers, build agents and lab management. I have customised the build workflow so that we can get a lot more functionality out of the automated build process. Some of these customisations are things like:

  • Finding the product version number
  • Incrementing the product version number
  • Updating the build name with the new product version
  • Updating files prior to compilation
  • Updating files after compilation
  • Building Sandcastle documentation
  • Deploying MSIs to remove servers
  • Filtering drop folder output (no source, just build output like configuration files, documentation and MSIs)

I use Codeplex at home for my personal projects. This means that I don’t get all the goodness that comes with TFSBuild in 2010. I still want the automatic version management functionality listed above however. I have the following functionality requirements to make this happen:

  • Determine the current product version number
  • Increment the product version number
    • Must be done prior to any project compilation
  • Sync Wix project versioning with product version number
    • Needs to happen before wix project compilation
    • Needs to cater for wix variables being used for version information
  • Allow TFS checkout for files under source control
    • This is important so that incremented version numbers continue to increment from previous version under source control
    • Must also cater for when solution is loaded without TFS availability (broken source control bindings etc)
  • Push product/wix version into wix output name
  • Failure to execute these actions successful will fail the solution/project build
  • No installation required
  • No configuration required
    • All customisation of tasks is done using command line arguments
  • Extensible using MEF
    • Again, no configuration required to add new tasks

The next post will outline how I was able to make this happen using an extensible MEF task based application.

WPF and the link that just won’t click

I’ve been playing with WPF over the last month. It has been great to finally work on a program that is well suited to this technology. One of the implementation details that has surprised me is that the Hyperlink control doesn’t do anything when you click the link.

I can only guess what the design reason is behind this. The only thing the control seems to do is fire off a RequestNavigate event. Every implementation of this control in a form then needs to manually handle this event to fire off the navigation uri. This is obviously going to be duplicated effort as each usage of the hyperlink control will execute the same logic to achieve this outcome.

I have put together the following custom control for my project to suit my purposes.

namespace Neovolve.Switch.Controls
{
    using System;
    using System.Diagnostics;
    using System.Windows.Documents;

    public class ClickableLink : Hyperlink
    {
        protected override void OnClick()
        {
            base.OnClick();

            Uri navigateUri = ResolveAddressValue(NavigateUri);

            if (navigateUri == null)
            {
                return;
            }

            String address = navigateUri.ToString();

            ProcessStartInfo startInfo = new ProcessStartInfo(address);
            
            Process.Start(startInfo);
        }
        
        private static Uri ResolveAddressValue(Uri navigateUri)
        {
            if (navigateUri == null)
            {
                return null;
            }

            // Disallow file urls
            if (navigateUri.IsFile)
            {
                return null;
            }

            if (navigateUri.IsUnc)
            {
                return null;
            }

            String address = navigateUri.ToString();

            if (String.IsNullOrWhiteSpace(address))
            {
                return null;
            }

            if (address.Contains("@") && address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase) == false)
            {
                address = "mailto:" + address;
            }
            else if (address.StartsWith("http://", StringComparison.OrdinalIgnoreCase) == false &&
                     address.StartsWith("https://", StringComparison.OrdinalIgnoreCase) == false)
            {
                address = "http://" + address;
            }

            try
            {
                return new Uri(address);
            }
            catch (UriFormatException)
            {
                return null;
            }
        }
    }
}

Neovolve ReSharper Plugins 2.0 released

It has been a few years since I have updated my ReSharper plugin project that was originally released three years ago. The reason for the project was that ReSharper natively converts CLR types (System.Int32) to its alias (int). StyleCop also adds this support in its ReSharper plugin. Unfortunately nothing in the marketplace provides a conversion from an alias type to its CLR type.

I always prefer the CLR types over the alias type names. This allows me to easily identify what is a C# keyword and what is a type reference.

The original 1.0 (and subsequent 1.1) version provided the ability to switch between C# alias types and their CLR type equivalents using ReSharper’s code cleanup profile support. This version adds code inspection and QuickFix support. It also adds better support for type conversions in xmldoc comments.

Code Cleanup

The code cleanup profile settings for this plugin are now found under the Neovolve category. This is the only change to the settings used for code cleanup.

image

Code Inspection

ReSharper code inspection allows for plugins to notify you when your code does not match defined inspection rules. This release of the plugin adds code inspection to detect when a value type is written in a way that is not desired according to the your ReSharper settings.

The code inspection settings for this plugin can be found under the Neovolve category.

image

Code highlights will then notify you when your code does not match the configured rules. For example, the settings in the above screenshot identify that type alias definitions show up as a suggestion to be converted to their CLR types. This can be seen below.

image

QuickFix

ReSharper has had quick fix support for several years. This version now supports QuickFix bulbs based on the information identified by the code inspection rules. Giving focus to the suggestion will then allow for a quick fix action using Alt + Enter.

image

This version adds better support for type identification in xmldoc comments. Code inspection and QuickFix support also come into play here as well.

image

You can grab this release from the CodePlex project here.

Tip for building a private domain controller for Lab Management with Network Isolation

There are obvious benefits by using Lab Management for testing your software. It is a fantastic environment for test teams to test the software written by a development team.

The requirement I had with my test labs was that I need to use domain controlled security within the test lab as this is what is used in production. I also do not want any impact on the development or production domains. The solution is to use a domain controller (DC) within the lab environment rather than reference the domain hosting the lab environment.

Having a test DC means that it needs to be isolated from the hosting network. This avoids AD, DNS and DHCP conflicts between the development and test networks. Lab management can be configured for network isolation to get around this problem. This means that the private DC will have a network connection that is private to the lab, while all the other machines in the lab will have one NIC for the private lab network and a second NIC for access out to the hosting environment. This setup can be seen in the SCVMM network diagram below with the machine at the top of the diagram being the private DC.

image

The problem I had for several weeks was that the private DC lost its Windows activation when it was stored into the VMM library for deployment out to a lab environment. You are restricted to phone activation in this case because once the stored VM is put into a lab with network isolation there is no internet support for automatic activation on the DC. This then needs to be done every time you deploy a lab environment.  

I followed the MSDN article steps that describe how to create a private DC for labs but there was nothing specific about how to handle this scenario. The step in question is at the bottom of the article where it says:

6. Shut down the virtual machine, and store it in the SCVMM library.

    a. Do not turn off the Active Directory VM. You have to shut it down correctly.

    b. Do not generalize the Active Directory VM, either by running Sysprep or by storing the virtual machine as a template in SCVMM.

I followed this step to the letter and stored my DC in the VMM library for use in labs. This was the step that caused the VM to lose its Windows activation. I happened to stumble across the solution to this problem as I had to rebuild the test DC yesterday. The answer is to clone the DC rather than store it.

image

The Clone wizard provides the option of where to place the clone VM. You want to select Store the virtual machine in the library.

image

The private DC can be deployed out to a lab environment now that it is stored in the library. The Windows activation is retained using this technique so the private DC should be ready for immediate use in the lab.

Beware of lifetime manager policy locks in Unity

I have created a caching dependency injection slice in order to squeeze more performance from the DAL in a workflow service. What I found was that the service always hit a timeout when the caching slice was put into the Unity configuration. I spent half a day working with AppFabric monitoring, event logs and all the information I could get out of diagnostic tracing for WCF, WF, WIF and custom sources. After not being able to get any answers along with futile debugging efforts, I realised that I could profile the service to see where the hold up was.

The profiler results told me exactly where to look as soon as I hit the service at got a timeout exception.

image

All the time is getting consumed in a single call to Microsoft.Practices.Unity.SynchronizedLifetimeManager.GetValue(). The first idea that comes to mind is that there is a lock on an object that is not being released. Reflector proves that this is exactly the case.

image

The GetValue method obtains a lock for the current thread and only releases it if a non-null value is held by the lifetime manager. This logic becomes a big issue if the lifetime manager holds a null value and two different threads call GetValue. I would like to know why this behaviour is there as it is intentional according to the documentation of the function.

This is what is happening in my service. In the profiling above you can see that the lifetime manager is getting called from my Unity extension for disposing build trees. While the extension is not doing anything wrong, it can handle this scenario by using a timeout on obtaining a value from the lifetime manager.

private static Object GetLifetimePolicyValue(ILifetimePolicy lifetimeManager)
{
    if (lifetimeManager is IRequiresRecovery)
    {
        // There may be a lock around this policy where a null value will result in an indefinite lock held by another thread
        // We need to use another thread to access this item so that we can get around the lock using a timeout
        Task<Object> readPolicyTask = new Task<Object>(lifetimeManager.GetValue);

        readPolicyTask.Start();

        Boolean taskCompleted = readPolicyTask.Wait(10);

        if (taskCompleted == false)
        {
            return null;
        }

        return readPolicyTask.Result;
    }

    return lifetimeManager.GetValue();
}

This implementation is not ideal, but it is unfortunately the only way to handle this case as there is no way to determine whether another thread has a lock on the lifetime manager.

Testing the service again with the profiler then identified a problem with this workaround.

image

This workaround will consume threads that will be held on a lock and potentially never get released. This is going to be unacceptable as more and more threads attempt to look at values held in the lifetime manager policies, ultimately resulting in thread starvation.

The next solution is to use a lifetime manager that gets around this issue by never allowing the lifetime manager to be assigned a null value.

namespace Neovolve.Jabiru.Server.Services
{
    using System;
    using System.Diagnostics.Contracts;
    using Microsoft.Practices.Unity;

    public class SafeSingletonLifetimeManager : ContainerControlledLifetimeManager
    {
        public override void SetValue(Object newValue)
        {
            if (newValue == null)
            {
                throw new ArgumentNullException("newValue");
            }

            base.SetValue(newValue);
        }
    }
}

This idea fails to get around the locking issue when the lifetime manager is created but never has a value assigned. The next version of this SafeSingletonLifetimeManager solves this by managing its own locking logic around whether a non-null value has been assigned to the policy.

vnamespace Neovolve.Jabiru.Server.Services
{
    using System;
    using System.Threading;
    using Microsoft.Practices.Unity;
    using Neovolve.Toolkit.Threading;

    public class SafeSingletonLifetimeManager : ContainerControlledLifetimeManager
    {
        private readonly ReaderWriterLockSlim _syncLock = new ReaderWriterLockSlim();

        private Boolean _valueAssigned;

        public override Object GetValue()
        {
            using (new LockReader(_syncLock))
            {
                if (_valueAssigned == false)
                {
                    return null;
                }

                return base.GetValue();
            }
        }

        public override void SetValue(Object newValue)
        {
            using (new LockWriter(_syncLock))
            {
                if (newValue == null)
                {
                    _valueAssigned = false;
                }
                else
                {
                    _valueAssigned = true;
                }

                base.SetValue(newValue);
            }
        }
    }
}

Using this policy now avoids the locking problem described in this post. I would still like to know the reason for the locking logic as this SafeSingletonLifetimeManager is completely circumventing that logic.

TFS and WF4: The diff noise problem

For a long time the most popular post I have on this site is about how to configure [VS] to use WinMerge as the merge/diff tool for TFS rather than using the feature poor out of the box software. Sometimes the nature of the files under development result in version differences that have a lot of noise regardless of the diff/merge tool that you use.

Unfortunately WF is one of the common offenders. I absolutely love WF, but am disappointed that designer state information is persisted with the workflow definition rather than in a user file that is merged in the IDE. The result of this is that the activity xaml file changes if you collapse a composite activity, such as the Sequence activity. The actual workflow definition has not changed, but it is a new version of the file as far as a diff tool and TFS goes.

For example, I have collapsed lots of activities on one of my workflows. The resulting diff using WinMerge looks like the following:

image

There is a lot of noise here. It is all designer state information rather than actual changes to the workflow definition. There are several culprits in WF4 that cause this noise.

  • sap:VirtualizedContainerService.HintSize
  • <x:Boolean x:Key="IsExpanded">
  • <x:Boolean x:Key="IsPinned">

Thankfully WinMerge has a great feature for applying a line filter expression (Tools –> Filters –> Linefilters). These can help to reduce a lot of this noise.

image

I have put together three expressions to cover WF4.

  • ^.*sap:VirtualizedContainerService\.HintSize="\d+,\d+".*$     filters sap:VirtualizedContainerService.HintSize when it is defined in an attribute
  • ^.*<sap:VirtualizedContainerService.HintSize>\d+,\d+</sap:VirtualizedContainerService.HintSize>.*$     filters sap:VirtualizedContainerService.HintSize when it is defined in an element
  • ^.*<x:Boolean x:Key="(IsExpanded|IsPinned)">.+</x:Boolean>.*$     filters <x:Boolean x:Key="IsExpanded"> and <x:Boolean x:Key="IsPinned"> elements

The above diff of workflow xaml with these filters applied now looks like the following.

image

There are several things to notice about the result of line filters. The overview of differences is now much less noisy. The file differences filtered by line filters are indicated by a light-yellow but do not show up in the diff overview or participate in keyboard navigation. The line filters are not able to filter all of the above WF4 issues as can be seen in the red change above. This is because the xml line is missing on one side rather than being just a change to the line. These changes to the xml still get picked up by the diff tool.

WinMerge stores the line filters in the registry. The easiest way to install them is to import the registry entry below.

WinMerge Line Filters.reg (978.00 bytes)