Returning Values From An AgentScope

Introduction

While Team Build will serialize workflow variables into an AgentScope, if they are updated within the AgentScope the updated values will not be serialized back when the AgentScope completes. To achieve this you need to attach the data you want to return to the IBuildDetail instance using information nodes.

There are three steps to this:

  1. Create an information node class that will store the data we want to return to the controller from the agent.
  2. In the AgentScope attach the information node to the IBuildDetail instance.
  3. After the AgentScope read the information node from the IBuildDetail instance.

In my (somewhat contrived) example, I’ll return some information (machine name, processor count, and operating system) from the AgentScope to the controller and write them out as a build message. You can download the custom activities and sample process template for this blog post from: https://cid-d0a4451bfb353deb.skydrive.live.com/self.aspx/.Public/InformationNodes.zip.

Creating an Information Node Class

Information nodes are simple classes that follow a specific pattern allowing them to be stored in the TFS database against the build detail. They have a name (allowing you to retrieve just the information nodes you’re interested in) as well as a series of fields (which are stored as name value pairs).

 using System;
using System.Globalization;
using Microsoft.TeamFoundation.Build.Client;
 
namespace InformationNodesActivities
{
    public class AgentInformationNode
    {
        public static readonly string Name = "AgentInformation";
 
        private IBuildInformationNode _Node;
        public AgentInformationNode(IBuildInformationNode node)
        {
            _Node = node;
        }
 
        private const string MachineNameFieldKey = "MachineName";
        public string MachineName
        {
            get
            {
                return _Node.Fields[MachineNameFieldKey];
            }
            set
            {
                _Node.Fields[MachineNameFieldKey] = value;
            }
        }
 
        private const string OSVersionFieldKey = "OSVersion";
        public string OSVersion
        {
            get 
            { 
                return _Node.Fields[OSVersionFieldKey]; 
            }
            set 
            { 
                _Node.Fields[OSVersionFieldKey] = value; 
            }
        }
 
        private const string ProcessorCountFieldKey = "ProcessorCount";
        public int ProcessorCount
        {
            get
            {
                return int.Parse(_Node.Fields[ProcessorCountFieldKey], CultureInfo.InvariantCulture);
            }
            set
            {
                _Node.Fields[ProcessorCountFieldKey] = value.ToString(CultureInfo.InvariantCulture);
            }
        }
 
        public void Save()
        {
            _Node.Save();
        }
    }
}

Storing the Information Node

Now that we have somewhere to store the values we want to pass from the AgentScope back to the controller we need to create an activity that creates one of these nodes, populates it, and then adds it to the build. We use the CreateNode method on IBuildDetail.Information to create an empty node, we then set its name (so we can retrieve it later), and create an instance of our custom information node class passing in the node we want it to store its values in.

 using System;
using System.Activities;
using InformationNodesActivities;
using Microsoft.TeamFoundation.Build.Client;
 
namespace InformationNodesActivities
{
    [BuildActivity(HostEnvironmentOption.Agent)]
    public sealed class CreateAgentInformationNode : CodeActivity
    {
        protected override void Execute(CodeActivityContext context)
        {
            var buildDetail = context.GetExtension<IBuildDetail>();
            
            var node = buildDetail.Information.CreateNode();
            node.Type = AgentInformationNode.Name;
 
            var agentInformationNode = new AgentInformationNode(node);
            agentInformationNode.MachineName = Environment.MachineName;
            agentInformationNode.OSVersion = Environment.OSVersion.ToString();
            agentInformationNode.ProcessorCount = Environment.ProcessorCount;
            agentInformationNode.Save();
        }
    }
}

Retrieving the Information Node

We can retrieve information nodes using one of the methods on IBuildDetail.Information, in our case we know the name of the node we want to retrieve, and know that there’s one and only one of them so we will use IBuildDetail.Information.GetNodesByType(string name).First().

Since we created the information node in the AgentScope and want to retrieve it from the controller once we’re back on the controller we’re going to call IBuildDetail.RefreshAllDetails to re-query all of the build’s details (including information nodes) from the database.

In the following build process template snippet we:

  1. Store the IBuildDetail instance in a variable.
  2. Enter the AgentScope.
  1. Call the custom CreateAgentInformationNode activity we created above.
  • Exit the AgentScope.
  • Refresh the IBuildDetail instance (using the InvokeMethod activity).
  • Retrieve the AgentInformationNode from the IBuildDetail instance (using the Assign activity).
  • Output values from the AgentInformationNode to the Team Build log (using the WriteBuildMessage activity).
 <Sequence>
  <Sequence.Variables>
    <Variable x:TypeArguments="mtbc1:IBuildDetail" Name="BuildDetail" />
    <Variable x:TypeArguments="i:AgentInformationNode" Name="RetrievedAgentInformationNode" />
  </Sequence.Variables>
  <mtbwa1:GetBuildDetail DisplayName="Get Build" Result="[BuildDetail]" />
  <mtbwa1:AgentScope>
    <i:CreateAgentInformationNode DisplayName="Create Node" />
  </mtbwa1:AgentScope>
  <InvokeMethod MethodName="RefreshAllDetails">
    <InvokeMethod.TargetObject>
      <InArgument x:TypeArguments="mtbc1:IBuildDetail">[BuildDetail]</InArgument>
    </InvokeMethod.TargetObject>
  </InvokeMethod>
  <Assign DisplayName="Read Node">
    <Assign.To>
      <OutArgument x:TypeArguments="i:AgentInformationNode">[RetrievedAgentInformationNode]</OutArgument>
    </Assign.To>
    <Assign.Value>
      <InArgument x:TypeArguments="i:AgentInformationNode">
        [New AgentInformationNode(BuildDetail.Information.GetNodesByType(AgentInformationNode.Name).First())]
      </InArgument>
    </Assign.Value>
  </Assign>
  <mtbwa1:WriteBuildMessage 
      DisplayName="Output Node Values" 
      Importance="[Microsoft.TeamFoundation.Build.Client.BuildMessageImportance.High]" 
      Message="[String.Format(&quot;Ran on agent {0} which has {1} processors and runs {2}.&quot;, 
        RetrievedAgentInformationNode.MachineName,
        RetrievedAgentInformationNode.ProcessorCount,
        RetrievedAgentInformationNode.OSVersion)]" />
</Sequence>