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:
- Create an information node class that will store the data we want to return to the controller from the agent.
- In the AgentScope attach the information node to the IBuildDetail instance.
- 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:
- Store the IBuildDetail instance in a variable.
- Enter the AgentScope.
- 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("Ran on agent {0} which has {1} processors and runs {2}.",
RetrievedAgentInformationNode.MachineName,
RetrievedAgentInformationNode.ProcessorCount,
RetrievedAgentInformationNode.OSVersion)]" />
</Sequence>
Comments
Anonymous
April 04, 2011
I am working on a custom template that builds the source on one agent and then uses several agents in parallel for InstallShield packaging (which for some reason is extremely slow). Using the above example, I tried to pass information from one agent via the controller to the multiple agents later in the flow. I then got the following error when spawning the second agent: "Unable to serialize type 'InformationNodesActivities.AgentInformationNode'. Verify that the type is public and either has a default constructor or an instance descriptor." Not quite familiar with this I tried to add an empty default constructor to AgentInformationNode, but this time I got the following error: "Exception has been thrown by the target of an invocation." Any ideas on what to do to solve my problem?Anonymous
April 04, 2011
Perhaps I should clarify that I assign a value from the AgentInformationNode to a variable in the controller (top) scope and the multiple agent were neaver meant to access the node. But somehow they do that anyway...Anonymous
July 27, 2011
Did you ever find a solution to this issue, Staffan?Anonymous
January 09, 2012
No, sadly no solution yet...Anonymous
January 07, 2014
How do you create the node if you use WF 4 and the visual representation of the XAML-Template? I cannot find any analogue to <i: ... /> which is the essentiel part for this example to work.