As part of our initiative to automate code deployments, we needed to move windows services from TFS to our test servers. The way we did this before was to have a build stage all the files and then either stop the services on the destination machine manually, copy files, and restart the services, or use a homebrewed application to deploy the service; which is fine and dandy, except that the original developer isn’t with the company anymore and the application is limited in certain ways.
So to solve this, I went ahead and created a new build template that would deploy our windows services from a TFS build.
Some prerequisites:
- PsExec must be on the build machine
- Your build service must be an admin on the machine you’re deploying to (to be able to stop and start services)
First, we’ll set up a new Code Activity, it’s very similar to the DeployFiles class i wrote about before
[csharp]
using System;
using System.Activities;
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.TeamFoundation.Build.Workflow.Activities;
using Microsoft.TeamFoundation.Build.Workflow.Tracking;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
namespace BuildProcess.Activities
{
[BuildActivity(HostEnvironmentOption.Agent)]
public sealed class DeployWindowsService : CodeActivity
{
//Source dir being deployed from
[RequiredArgument]
public InArgument<string> SourceDir { get; set; }
//Destination dir being copied from
[RequiredArgument]
public InArgument<string[]> DestinationDir { get; set; }
//Files to include
[RequiredArgument]
public InArgument<string> FileInclusions { get; set; }
//Folders to include
[RequiredArgument]
public InArgument<string> FolderInclusions { get; set; }
[RequiredArgument]
public InArgument<string> CollectionName { get; set; }
public InArgument<string> ConfigLocations { get; set; }
//globals
public string fileInclusionPattern = "";
public string folderInclusionPattern = "";
protected override void Execute(CodeActivityContext context)
{
// Obtain the runtime value of the Text input argument
string fileInclusions = context.GetValue(this.FileInclusions);
string folderInclusions = context.GetValue(this.FolderInclusions);
string configLocations = context.GetValue(this.ConfigLocations);
string[] destinations = context.GetValue(this.DestinationDir);
string collectionName = context.GetValue(this.CollectionName);
DirectoryInfo sourceDir = new DirectoryInfo(context.GetValue(this.SourceDir));
//parse exclusions, add them to regex patterns
if (!String.IsNullOrWhiteSpace(fileInclusions))
{
string[] fileinarr = fileInclusions.Split(‘,’);
fileInclusionPattern = "(";
foreach (string s in fileinarr)
{
fileInclusionPattern += s.ToUpper().Trim().Replace(".", @"\.").Replace("*", @"[a-zA-Z0-9]*") + "$|";
}
if (fileInclusionPattern.EndsWith("|"))
fileInclusionPattern = fileInclusionPattern.Substring(0, fileInclusionPattern.Length – 1);
fileInclusionPattern += ")";
}
if (!String.IsNullOrWhiteSpace(folderInclusions))
{
string[] folderinarr = folderInclusions.Split(‘,’);
folderInclusionPattern = "(";
foreach (string s in folderinarr)
{
folderInclusionPattern += s.ToUpper().Trim().Replace(".", @"\.").Replace("*", @"[a-zA-Z0-9]*") + "$|";
}
if (folderInclusionPattern.EndsWith("|"))
folderInclusionPattern = folderInclusionPattern.Substring(0, folderInclusionPattern.Length – 1);
folderInclusionPattern += ")";
}
foreach (string dir in destinations)
{
DirectoryInfo destDir = new DirectoryInfo(dir);
//Used for debugging, you don’t need this, unless you want to display something custom here.
context.Track(new BuildInformationRecord<BuildMessage>()
{
Value = new BuildMessage()
{
Importance = BuildMessageImportance.High,
Message = "Source: " + sourceDir.FullName +
"\r\nDestination: " + destDir.FullName +
"\r\nFolder Inclusion Pattern: " + folderInclusionPattern +
"\r\nFile Inclusion Pattern: " + fileInclusionPattern +
"\r\nFolder Inclusions String: " + folderInclusions +
"\r\nFile Inclusions String: " + fileInclusions,
},
});
CopyDir(sourceDir, destDir);
}
//copy config files
if (!String.IsNullOrWhiteSpace(configLocations))
{
string strTFSName = collectionName;
//set up TFS connectivity
Uri tfsName = new Uri(strTFSName);
TfsTeamProjectCollection tfs = new TfsTeamProjectCollection(tfsName);
VersionControlServer vcs = (VersionControlServer)tfs.GetService(typeof(VersionControlServer));
//copy config files to proper location
string timestamp = DateTime.Now.Ticks.ToString();
string workspacePath = @"C:\temp\TFSConfigDeployment_" + timestamp;
Workspace ws = vcs.CreateWorkspace("TFSConfigDeployment_" + timestamp, vcs.AuthorizedUser);
//get the workspace locally
ws.Map(configLocations, workspacePath);
ws.Get();
foreach (string configDir in destinations)
{
CopyDir(new DirectoryInfo(workspacePath), new DirectoryInfo(configDir));
}
RemoveWorkSpaceDirectory(workspacePath);
}
}
private void RemoveWorkSpaceDirectory(string workspacePath)
{
DirectoryInfo dir = new DirectoryInfo(workspacePath);
var files = dir.EnumerateFiles();
foreach (FileSystemInfo info in files)
{
string filePath = info.FullName;
if ((File.GetAttributes(filePath) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(filePath, FileAttributes.Normal);
}
}
var subDirs = dir.EnumerateDirectories();
foreach (DirectoryInfo subDir in subDirs)
{
RemoveWorkSpaceDirectory(subDir.FullName);
}
dir.Delete(true);
}
private void CopyDir(DirectoryInfo sourceDir, DirectoryInfo destDir)
{
//create destination dir if it doesn’t exist
if (!destDir.Exists)
{
destDir.Create();
}
// get all files from current dir
FileInfo[] files = sourceDir.GetFiles();
//copy ze files!!
foreach (FileInfo file in files)
{
string destFilePath = Path.Combine(destDir.FullName, file.Name);
if (!String.IsNullOrWhiteSpace(fileInclusionPattern))
{
if (Regex.IsMatch(file.Name.ToUpper(), fileInclusionPattern))
{
if (File.Exists(destFilePath))
{
if ((File.GetAttributes(destFilePath) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(destFilePath, FileAttributes.Normal);
}
File.Delete(destFilePath);
}
file.CopyTo(destFilePath);
if ((File.GetAttributes(destFilePath) & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
{
File.SetAttributes(destFilePath, FileAttributes.Normal);
}
}
}
}
// get subdirectories.
DirectoryInfo[] dirs = sourceDir.GetDirectories();
foreach (DirectoryInfo dir in dirs)
{
// Get destination directory.
string destinationDir = Path.Combine(destDir.FullName, dir.Name);
if (!String.IsNullOrWhiteSpace(folderInclusionPattern))
{
if (Regex.IsMatch(dir.Name.ToUpper(), folderInclusionPattern))
{
// Call CopyDirectory() recursively.
CopyDir(dir, new DirectoryInfo(destinationDir));
}
}
}
}
}
}
[/csharp]
For the next step, you’ll also want to look at the previous post about deploying files from TFS. After you’ve opened up your ProcessTemplate, you’ll want to have the following arguements:
- DeploymentDir – In – String[]
- SourceRootDir – In – String
- CollectionName – In – String
- PsExecPath – In – String
- ServiceName – In – String
- ServerName – In – String
- FileInclusions – In – String
- FolderInclustions – In – String
- RestartService – In – Boolean – True
Each of these you’ll probably want to add to the Metadata Argument so you can keep your build definitions organized.
And you’ll want to add the following Variable:
- PsExecResult – Int32 – Revert Workspace and Copy Files…
Under Process > Sequence > Run On Agent > Try Compile, Test, and Associate Changesets and Work Items > Revert Workspace and Copy Files to Drop Location > If Deployment Location is Set you’ll want to place a new Sequence and call it Stop Service, Copy Files, Start Service.
Inside that Sequence you’ll want to place an Invoke Process activity, the new DeployWindowsServices code activity, and an If statement with another Invoke Process activity in the Then clause.
For the first Process Invocation:
Arguements: ServerName + ” -s -d net stop ” + ServiceName
DisplayName: Stop Service
FileName: PsExecPath
Result: PsExecResult
For the Deployment Activity:
CollectionName: CollectionName
DestinationDir: DeploymentDir
DisplayName: Deploy Windows Service
FileInclustions: FileInclusions
FolderInclustions: FolderInclusions
SourceDir: BinariesDirectory + “\” + SourceRootDir
For the If statement, just check to see if RestartService is set to True, and then enter the values for the second Process Invocation:
Arguements: ServerName + ” -s -d net start ” + ServiceName
DisplayName: Start Service
FileName: PsExecPath
*NOTE, THE FOLLOWING SECTION DOES NOT WORK PROPERLY YET*
After the If statement, you’ll want to add in another If, this will check to see if PsExec has failed or not.
Since this isn’t working properly yet, i just check to see if PsExecResult is ‘1’, which it hasn’t been as of this writing. In the Then block, add a Throw activity and have it throw an exception of:
[csharp]
New Exception("PsExecResult: " + PsExecResult.ToString())
[/csharp]
Optionally, you can also add WriteBuildMessage and WriteBuildWarning activities to your Invoke Process Activities, these are nice because you can see what the process you are invoking outputs.