11 min read

In this article by Gordon Beeming, the author of the book, Team Foundation Server 2015 Customization, we are going to cover TFS scheduled jobs. The topics that we are going to cover include:

  • Writing a TFS Job
  • Deploying a TFS Job
  • Removing a TFS Job

You would want to write a scheduled job for any logic that needs to be run at specific times, whether it is at certain increments or at specific times of the day. A scheduled job is not the place to put logic that you would like to run as soon as some other event, such as a check-in or a work item change, occurs.

It will automatically link change sets to work items based on the comments.

(For more resources related to this topic, see here.)

The project setup

First off, we’ll start with our project setup. This time, we’ll create a Windows console application.

Creating a new windows console application

The references that we’ll need this time around are:

  • Microsoft.VisualStudio.Services.WebApi.dll
  • Microsoft.TeamFoundation.Common.dll
  • Microsoft.TeamFoundation.Framework.Server.dll

All of these can be found in C:Program FilesMicrosoft Team Foundation Server 14.0Application TierTFSJobAgent on the TFS server.

That’s all the setup that is required for your TFS job project. Any class that inherit ITeamFoundationJobExtension will be able to be used for a TFS Job.

Writing the TFS job

So, as mentioned, we are going to need a class that inherits from ITeamFoundationJobExtension.

Let’s create a class called TfsCommentsToChangeSetLinksJob and inherit from ITeamFoundationJobExtension. As part of this, we will need to implement the Run method, which is part of an interface, like this:

public class TfsCommentsToChangeSetLinksJob : ITeamFoundationJobExtension
{
public TeamFoundationJobExecutionResult Run(
     TeamFoundationRequestContext requestContext,
     TeamFoundationJobDefinition jobDefinition,
     DateTime queueTime, out string resultMessage)
{
   throw new NotImplementedException();
}
}

Then, we also add the using statement:

using Microsoft.TeamFoundation.Framework.Server;

Now, for this specific extension, we’ll need to add references to the following:

  • Microsoft.TeamFoundation.Client.dll
  • Microsoft.TeamFoundation.VersionControl.Client.dll
  • Microsoft.TeamFoundation.WorkItemTracking.Client.dll

All of these can be found in C:Program FilesMicrosoft Team Foundation Server 14.0Application TierTFSJobAgent.

Now, for the logic of our plugin, we use the following code inside of the Run method as a basic shell, where we’ll then place the specific logic for this plugin. This basic shell will be adding a try catch block, and at the end of the try block, it will return a successful job run. We’ll then add to the job message what exception may be thrown and returning that the job failed:

resultMessage = string.Empty;
try
{
// place logic here

return TeamFoundationJobExecutionResult.Succeeded;
}
catch (Exception ex)
{
resultMessage += "Job Failed: " + ex.ToString();
return TeamFoundationJobExecutionResult.Failed;
}

Along with this code, you will need the following using function:

using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using System.Linq;
using System.Text.RegularExpressions;

So next, we need to place some logic specific to this job in the try block. First, let’s create a connection to TFS for version control:

TfsTeamProjectCollection tfsTPC =
       TfsTeamProjectCollectionFactory.GetTeamProjectCollection(
         new Uri("http://localhost:8080/tfs"));
VersionControlServer vcs =
       tfsTPC.GetService<VersionControlServer>();

Then, we will query the work item store’s history and get the last 25 check-ins:

WorkItemStore wis = tfsTPC.GetService<WorkItemStore>();
// get the last 25 check ins
foreach (Changeset changeSet in vcs.QueryHistory("$/",
RecursionType.Full, 25))
{
   // place the next logic here
}

Now that we have the changeset history, we are going to check the comments for any references to work items using a simple regex expression:

//try match the regex for a hash number in the comment
foreach (Match match in Regex.Matches((changeSet.Comment
   ?? string.Empty), @"#d{1,}"))
{
   // place the next logic here
}

Getting into this loop, we’ll know that we have found a valid number in the comment and that we should attempt to link the check-in to that work item. But just the fact that we have found a number doesn’t mean that the work item exists, so let’s try find a work item with the found number:

int workItemId = Convert.ToInt32(match.Value.TrimStart('#'));
var workItem = wis.GetWorkItem(workItemId);
if (workItem != null)
{
   // place the next logic here
}

Here, we are checking to make sure that the work item exists so that if the workItem variable is not null, then we’ll proceed to check whether a relationship for this changeSet and workItem function already exists:

//now create the link
ExternalLink changesetLink = new ExternalLink(
wis.RegisteredLinkTypes[ArtifactLinkIds.Changeset],
changeSet.ArtifactUri.AbsoluteUri);
//you should verify if such a link already exists
if (!workItem.Links.OfType<ExternalLink>()
   .Any(l => l.LinkedArtifactUri ==
   changeSet.ArtifactUri.AbsoluteUri))
{
// place the next logic here
}

If a link does not exist, then we can add a new link:

changesetLink.Comment = "Change set " +
       $"'{changeSet.ChangesetId}'" +
       " auto linked by a server plugin";
workItem.Links.Add(changesetLink);
workItem.Save();
resultMessage += $"Linked CS:{changeSet.ChangesetId} " +
                 $"to WI:{workItem.Id}";

We just have the extra bit here so as to get the last 25 change sets. If you were using this for production, you would probably want to store the last change set that you processed and then get history up until that point, but I don’t think it’s needed to illustrate this sample.

Then, after getting the list of change sets, we basically process everything 100 percent as before. We check whether there is a comment and whether that comment contains a hash number that we can try linking to a changeSet function. We then check whether a workItem function exists for the number that we found. Next, we add a link to the work item from the changeSet function. Then, for each link we add to the overall resultMessage string so that when we look at the results from our job running, we can see which links were added automatically for us.

As you can see, with this approach, we don’t interfere with the check-in itself but rather process this out-of-hand way of linking changeSet to work with items at a later stage.

Deploying our TFS Job

Deploying the code is very simple; change the project’s Output type to Class Library. This can be done by going to the project properties, and then in the Application tab, you will see an Output type drop-down list. Now, build your project. Then, copy the TfsJobSample.dll and TfsJobSample.pdb output files to the scheduled job plugins folder, which is C:Program FilesMicrosoft Team Foundation Server 14.0Application TierTFSJobAgentPlugins.

Unfortunately, simply copying the files into this folder won’t make your scheduled job automatically installed, and the reason for this is that as part of the interface of the scheduled job, you don’t specify when to run your job. Instead, you register the job as a separate step. Change Output type back to Console Application option for the next step. You can, and should, split the TFS job from its installer into different projects, but in our sample, we’ll use the same one.

Registering, queueing, and deregistering a TFS Job

If you try install the job the way you used to in TFS 2013, you will now get the TF400444 error:

TF400444: The creation and deletion of jobs is no longer supported. You may only update the EnabledState or Schedule of a job. Failed to create, delete or update job id 5a7a01e0-fff1-44ee-88c3-b33589d8d3b3

This is because they have made some changes to the job service, for security reasons, and these changes prevent you from using the Client Object Model. You are now forced to use the Server Object Model.

The code that you have to write is slightly more complicated and requires you to copy your executable to multiple locations to get it working properly. Place all of the following code in your program.cs file inside the main method.

We start off by getting some arguments that are passed through to the application, and if we don’t get at least one argument, we don’t continue:

#region Collect commands from the args
if (args.Length != 1 && args.Length != 2)
{
Console.WriteLine("Usage: TfsJobSample.exe <command "+
                    "(/r, /i, /u, /q)> [job id]");
return;
}
string command = args[0];
Guid jobid = Guid.Empty;
if (args.Length > 1)
{
if (!Guid.TryParse(args[1], out jobid))
{
   Console.WriteLine("Job Id not a valid Guid");
   return;
}
}
#endregion

We then wrap all our logic in a try catch block, and for our catch, we only write the exception that occurred:

try
{
// place logic here
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}

Place the next steps inside the try block, unless asked to do otherwise. As part of using the Server Object Model, you’ll need to create a DeploymentServiceHost. This requires you to have a connection string to the TFS Configuration database, so make sure that the connection string set in the following is valid for you. We also need some other generic path information, so we’ll mimic what we could expect the job agents’ paths to be:

#region Build a DeploymentServiceHost
string databaseServerDnsName = "localhost";
string connectionString = $"Data Source={databaseServerDnsName};"+
"Initial Catalog=TFS_Configuration;Integrated Security=true;";
TeamFoundationServiceHostProperties deploymentHostProperties =
   new TeamFoundationServiceHostProperties();
deploymentHostProperties.HostType =
             TeamFoundationHostType.Deployment |
             TeamFoundationHostType.Application;
deploymentHostProperties.Id = Guid.Empty;
deploymentHostProperties.PhysicalDirectory =
   @"C:Program FilesMicrosoft Team Foundation Server 14.0"+
   @"Application TierTFSJobAgent";
deploymentHostProperties.PlugInDirectory =
   $@"{deploymentHostProperties.PhysicalDirectory}Plugins";
deploymentHostProperties.VirtualDirectory = "/";
ISqlConnectionInfo connInfo =
   SqlConnectionInfoFactory.Create(connectionString,
                                  null, null);
DeploymentServiceHost host =
   new DeploymentServiceHost(deploymentHostProperties,
                             connInfo, true);
#endregion

Now that we have a TeamFoundationServiceHost function, we are able to create a TeamFoundationRequestContext function . We’ll need it to call methods such as UpdateJobDefinitions, which adds and/or removes our job, and QueryJobDefinition, which is used to queue our job outside of any schedule:

using (TeamFoundationRequestContext requestContext =
                             host.CreateSystemContext())
{
TeamFoundationJobService jobService =
     requestContext.GetService<TeamFoundationJobService>()
// place next logic here
}

We then create a new TeamFoundationJobDefinition instance with all of the information that we want for our TFS job, including the name, schedule, and enabled state:

var jobDefinition =
   new TeamFoundationJobDefinition(
           "Comments to Change Set Links Job",
           "TfsJobSample.TfsCommentsToChangeSetLinksJob");
jobDefinition.EnabledState =
     TeamFoundationJobEnabledState.Enabled;
jobDefinition.Schedule.Add(new TeamFoundationJobSchedule
{ 
ScheduledTime = DateTime.Now,
PriorityLevel = JobPriorityLevel.Normal,
Interval = 300,
});

Once we have the job definition, we check what the command was and then execute the code that will relate to that command. For the /r command, we will just run our TFS job outside of the TFS job agent:

if (command == "/r")
{
string resultMessage;
new TfsCommentsToChangeSetLinksJob().Run(requestContext,
                 jobDefinition, DateTime.Now, out resultMessage);
}

For the /i command, we will install the TFS job:

else if (command == "/i")
{
jobService.UpdateJobDefinitions(requestContext, null,
   new[] { jobDefinition });
}

For the /u command, we will uninstall the TFS Job:

else if (command == "/u")
{
jobService.UpdateJobDefinitions(requestContext,
   new[] { jobid }, null);
}

Finally, with the /q command, we will queue the TFS job to be run inside the TFS job agent and outside of its schedule:

else if (command == "/q")
{
jobService.QueryJobDefinition(requestContext, jobid);
}

Now that we have this code in the program.cs file, we need to compile the project and then copy TfsJobSample.exe and TfsJobSample.pdb to the TFS Tools folder, which is C:Program FilesMicrosoft Team Foundation Server 14.0Tools. Now open a cmd window as an administrator. Change the directory to the Tools folder and then run your application with a /i command, as follows:

Installing the TFS Job

Now, you have successfully installed the TFS Job. To uninstall it or force it to be queued, you will need the job ID. But basically you have to run /u with the job ID to uninstall, like this:

Uninstalling the TFS Job

You will be following the same approach as prior for queuing, simply specifying the /q command and the job ID.

How do I know whether my TFS Job is running?

The easiest way to check whether your TFS Job is running or not is to check out the job history table in the configuration database. To do this, you will need the job ID (we spoke about this earlier), which you can obtain by running the following query against the TFS_Configuration database:

SELECT JobId
FROM Tfs_Configuration.dbo.tbl_JobDefinition WITH ( NOLOCK )
WHERE JobName = 'Comments to Change Set Links Job'

With this JobId, we will then run the following lines to query the job history:

SElECT *
FROM Tfs_Configuration.dbo.tbl_JobHistory WITH (NOLOCK)
WHERE JobId = '<place the JobId from previous query here>'

This will return you a list of results about the previous times the job was run. If you see that your job has a Result of 6 which is extension not found, then you will need to stop and restart the TFS job agent. You can do this by running the following commands in an Administrator cmd window:

net stop TfsJobAgent
net start TfsJobAgent

Note that when you stop the TFS job agent, any jobs that are currently running will be terminated. Also, they will not get a chance to save their state, which, depending on how they were written, could lead to some unexpected situations when they start again.

After the agent has started again, you will see that the Result field is now different as it is a job agent that will know about your job. If you prefer browsing the web to see the status of your jobs, you can browse to the job monitoring page (_oi/_jobMonitoring#_a=history), for example, http://gordon-lappy:8080/tfs/_oi/_jobMonitoring#_a=history. This will give you all the data that you can normally query but with nice graphs and grids.

Summary

In this article, we looked at how to write, install, uninstall, and queue a TFS Job. You learned that the way we used to install TFS Jobs will no longer work for TFS 2015 because of a change in the Client Object Model for security.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here