Third Shelf

Automating a ClickOnce Deployment – Part3

Posted in team foundation server by Sydney du Plooy on March 20, 2010

Continuing the series on Automating a ClickOnce Deployment it’s time to implement the deployment scheme in a build file.

After the build file compiled the solution we are ready to start the copying of the installation files to the deployment server. Before we start the copy process we have to create a new directory for the new application version. In the following snippet I simply generate a new version number and then create a directory where we will copy the installation files to:

<Target Name="DeployApplication">
  <Message Text="Deploying the client application to $(PublishServer)" />
  <MakeDir Directories="$(DeploymentServer)\Published\App\$(VersionNumber)" />

We are ready to copy the application files to the server:

<Message Text="Copying files to deployment directory [$(DeploymentServer)\Published\App\$(VersionNumber)]" />
<Copy DestinationFiles="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension)')" SourceFiles="@(ApplicationFiles->'$(BinariesFolder)\%(Filename)%(Extension)')" />

The ApplicationFiles is an item group with the installation files that we are copying. Note: we do not transform the filename to include the .deploy extension because we first need to generate the deployment manifest with the files in the right location before changing the name of the files. (See Gotcha #3). Next, we create the application manifest with the Mage utility which will be stored in the application directory (not in the version directory). Mage is a manifest generator and is used to generate both application and deployment manifests. In the snippet below we are using Mage explicitly to generate the application manifest:

<CreateProperty Value="$(DeploymentServer)\Published\App\$(VersionNumber)\App.exe.manifest">
  <Output TaskParameter="Value" PropertyName="ApplicationManifestFile"/>
</CreateProperty>
<Message Text="Generating application manifest  [$(ApplicationManifestFile)] using  [$(DeploymentServer)\Published\App\$(VersionNumber)]" />
<Exec Command="mage.exe -New  Application -p msil -TrustLevel FullTrust -ToFile  $(ApplicationManifestFile) -Name  &quot;App&quot; -Version $(VersionNumber) -FromDirectory  $(DeploymentServer)\Published\App\$(VersionNumber)" />

So far so good, we have now managed to generate the deployment manifest from the files located in the …\Published\App directory and placed the file in the …\Published\App\$(VersionNumber) directory. We will now point the application manifest to this deployment manifest so that the correct files are installed on the client machine.

We now have everything that we need in order to generate the application manifest.

<CreateItem Include="$(ApplicationManifestFile)" AdditionalMetadata="TargetPath=$(VersionNumber)\App.exe.manifest">
  <Output TaskParameter="Include" ItemName="RelativeApplicationManifestFile"/>
</CreateItem>

<GenerateDeploymentManifest AssemblyName="App.exe.application"
  AssemblyVersion="$(VersionNumber)"
  DeploymentUrl="http://deployment-server/Published/App/App.exe.application"
  Product="App"
  Description="App"
  Publisher="thirdshelf.com"
  Install="true"
  UpdateEnabled="true"
  UpdateMode="Foreground"
  OutputManifest="$(DeploymentServer)\Published\App\App.exe.application"
  MapFileExtensions="true"
  EntryPoint="@(RelativeApplicationManifestFile)" />

If you read part 2 of this series you should be able to map these properties straight to their positions in the application manifest file and their functions. The only thing that remains is to rename the files to include their .deploy extensions. This can easily be done in the following way:

<Message Text="Replacing files in [$(DeploymentServer)\Published\App\$(VersionNumber)\] with [.deploy]" />
<Delete Files="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension)')" />
<Copy SourceFiles="@(ApplicationFiles->'$(BinariesFolder)\%(Filename)%(Extension)')
  DestinationFiles="@(ApplicationFiles->'$(DeploymentServer)\Published\App\$(VersionNumber)\%(Filename)%(Extension).deploy')" />

Visiting the URL http://deployment-server/Published/App/App.exe.manifest and clicking on the Install button you should see the following dialog:

Gotchas

There are a few gotchas that you might find on your way to success and I’ve listed the most common ones here and their solutions for your convenience:

Gotcha #1: Reference in the manifest does not match the identity of the downloaded assembly …

This is easily resolved by adding the NoWin32Manifest property to solution in the SolutionsToBuild group:

<SolutionToBuild Include="$(SolutionRoot)\Application.sln">
  <Properties>NoWin32Manifest=true</Properties>
</SolutionToBuild>

Gotcha #2: XML files are marked as data files, by default.

This means that XML files are published to the data directory whose location can found by querying the System.Deployment.Application.ApplicationDeployment.CurrentDeployment.DataDirectory  property.

Gotcha #3: The customHostSpecified attribute is not supported for Windows Forms applications.

Not a very helpful error, but chances are that you generated the deployment manifest with the installation files having the .deploy extension and not on the original filenames.

Tagged with: , ,

Automating a ClickOnce Deployment – Part2

Posted in team foundation server by Sydney du Plooy on March 20, 2010

Continuing the series on Automating a ClickOnce Deployment it’s time to have a look at the manifest files. There are two required manifest files that give clickonce the necessary information to deploy the application:

  • an application manifest (.application);
  • a deployment manifest (.manifest).

Application Manifest

The application manifest describes the application that we are going to install. So, let’s have a look at the contents of this file:

xml version="1.0" encoding="utf-8"?>
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xrml="urn:mpeg:mpeg21:2003:01-REL-R-NS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity name="App.application" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />
  asmv2:publisher="App Publishers Company" asmv2:product="App" xmlns="urn:schemas-microsoft-com:asm.v1">App
  mapFileExtensions="true">
    <subscription>
      <update>
        <beforeApplicationStartup />
      </update>
    </subscription>
    <deploymentProvider codebase="http://deployment-server/Published/App/app.exe.application" />
  </deployment>
  <dependency>
    <dependentAssembly dependencyType="install" codebase="1.0.903.18476\App.exe.manifest" size="33600">
      <assemblyIdentity name="App.exe" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" />
      <hash>
        <dsig:Transforms>
          <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <dsig:DigestValue>DrLiyVjtlKhOmtuzuXhFSTqi/9Q=
      </hash>
    </dependentAssembly>
  </dependency>
</asmv1:assembly>

One of the important things to note in this manifest is the mapFileExtensions attribute in the deployment tag. This attribute indicates that our installation files have a .deploy extension – MyApplication.exe is copied to the deployment server as MyApplication.exe.deploy and is renamed when installed on the client machine. In the deploymentProvider tag we specify where the application can find future updates and from where this application was installed.

The next interesting tag is dependentAssembly, which points to the deployment manifest to use when installing the application on the client machine. We specify the deployment manifest in the codeBase attribute which describes how the application installs. Note that the codebase attribute points to a version folder in which the deployment manifest resides – for every new release of the application this application manifest is either overwritten or updated to point to the new version of the installation files and deployment manifest.

Deployment Manifest

The deployment manifest describes the how the application installs and what files to install on the client. Once again, let’s have a look at the contents:

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly xsi:schemaLocation="urn:schemas-microsoft-com:asm.v1 assembly.adaptive.xsd" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#" xmlns="urn:schemas-microsoft-com:asm.v2" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:co.v1="urn:schemas-microsoft-com:clickonce.v1">
  <asmv1:assemblyIdentity name="App.exe" version="1.0.903.18476" publicKeyToken="0000000000000000" language="neutral" processorArchitecture="msil" type="win32" />
  <application />
  <entryPoint>
    <assemblyIdentity name="App" version="1.0.903.18476" language="neutral" processorArchitecture="msil" />
    <commandLine file="App.exe" parameters="" />
  </entryPoint>
  <trustInfo>
    <security>
      <applicationRequestMinimum>
        <PermissionSet Unrestricted="true" ID="Custom" SameSite="site" />
        <defaultAssemblyRequest permissionSetReference="Custom" />
      </applicationRequestMinimum>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
  <dependency>
    <dependentOS>
      <osVersionInfo>
        <os majorVersion="5" minorVersion="1" buildNumber="0" servicePackMajor="0" />
      </osVersionInfo>
    </dependentOS>
  </dependency>
  <dependency>
    <dependentAssembly dependencyType="preRequisite" allowDelayedBinding="true">
      <assemblyIdentity name="Microsoft.Windows.CommonLanguageRuntime" version="2.0.50727.0" />
    </dependentAssembly>
  </dependency>
  <dependency>
    <dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="DevExpress.Data.v9.1.dll" size="855552">
      <assemblyIdentity name="App" version="9.1.5.0" publicKeyToken="B88D1754D700E49A" language="neutral" processorArchitecture="msil" />
      <hash>
        <dsig:Transforms>
          <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
        </dsig:Transforms>
        <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <dsig:DigestValue>CXehkPHON56TeTj7ZWsy++QsymE=
      </hash>
    </dependentAssembly>
  </dependency>
  <file name="GlobalConfiguration.xml" size="21074">
    <hash>
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>6hHtbL9F9sfgWmKLYrWRpYmILCE=
    </hash>
  </file>
</asmv1:assembly>

The first tag of interest here is the entryPoint tag that identifies the application to run once the installation completes. In order to start the application clickonce will execute the command with the parameters specified in the commandLine tag.

Every application has dependencies that must either be installed with the program or should have been installed. In the first dependency tag we see that the minimum operating system version required is Windows XP. In the second dependency tag we see that the dependencyType attribute instructs clickonce to verify that the .NET Framework 2.0 is already installed on the client.

In the third dependency tag we see that the dependencyType is install, which instructs clickonce to install this dependency on the client. Remember that the .deploy extension will automatically be removed during the installation process.

The next tag is a file tag that specifies a non-assembly file that is copied into the application installation directory. Note: by default when clickonce encounters a xml file it assumes it as a configuration file and will copy it to a data directory; along the application directory; instead of the application directory. You can find the data directory location by querying the System.Deployment.ApplicationApplicationDeployment.DataDirectory property.

In the next installment of this series we will show a build file example and generate a deployment manifest with the Mage tool.

Tagged with: , ,

Executing a task remotely with MsBuild

Posted in team foundation server by Sydney du Plooy on October 19, 2009

Recently, we were in need of executing a command on a remote server as part of our automated build process. For this we decided to make use of Windows Management Instrumentation (WMI). Before jumping to the code, copying and pasting it, there are always some security issues that need to be addressed. For this task there are also some service configurations that are required on the remote machine.

Note: All of the following settings are configured on the remote machine on which the command will be run remotely.

Security

Follow the steps below to set the WMI permissions for the build service account:

  1. Click on Start > Control Panel > Administrative Tools > Computer Management.
  2. Expand Service and Applications.
  3. Right-click on WMI Control and click on Properties.
  4. On the property dialog select the Security tab.
  5. Expand Root > CIMV2.
  6. Click on the Security button.
  7. Click on Add and add <TFSSERVICE> account.
  8. In the list of permissions, allow the following permissions:
    • Execute methods
    • Remote enable
  9. Click on OK.

Follow the step below to set the Local Security Policy for the build service account:

  1. Click on Start > Control Panel > Administrative Tools > Local Security Policy.
  2. Expand Local Policies > User Rights Assignment.
  3. Find and double-click on Log on as a service.
  4. Click on Add User or Group and add <TFSSERVICE> account.
  5. Click on OK.
  6. Find and double-click Log on as a batch job.
  7. Click on Add User or Group and add <TFSSERVICE> account.
  8. Click on OK and close the Local Security Policy window.

Services

Follow the steps below set the dependent services to automatically start:

  1. Click on Start > Control Panel > Administrative Tools.
  2. Double click on Services.
  3. Ensure that the following services are Started, and are set to start Automatic.
    • COM+ Event System
    • Remote Access Auto Connection Manager
    • Remote Access Connection Manager
    • Remote Procedure Call (RPC)
    • Remote Procedure Call (RPC) Locator
    • Remote Registry
    • Server
    • Windows Management Instrumentation
    • Windows Management Instrumentation Driver Extensions
    • WMI Performance Adapter
    • Workstation
  4. Close the services dialog.

Here is the code for the build task that will execute a command on a remote machine:

/// <summary>
/// Executes a command line on a remote machine.
/// </summary>
public class RemoteExec : Task
{
 /// <summary>
 /// Initialises a new instance of the <see cref="RemoteExec"/> class.
 /// </summary>
 public RemoteExec()
 {
   RemoteMachine = Environment.MachineName;
 }

 /// <summary>
 /// Gets or sets the name of the machine on which to execute the command.
 /// </summary>
 public string RemoteMachine
 {
   get;
   set;
 }

 /// <summary>
 /// Gets or sets the command to execute on the remote machine.
 /// </summary>
 [Required]
 public string Command
 {
   get;
   set;
 }

 /// <summary>
 /// Executes the task on the remote machine.
 /// </summary>
 /// <returns>true if the task succeeded, otherwise, false</returns>
 public override bool Execute()
 {
   if (string.IsNullOrEmpty(Command))
   {
     Log.LogError("Command property was not set.");
     throw new ArgumentNullException("Command");
   }

   var connOptions = new ConnectionOptions();
   connOptions.Impersonation = ImpersonationLevel.Impersonate;
   connOptions.EnablePrivileges = true;

   var managementScope = new ManagementScope(
     string.Format(@"\\{0}\ROOT\CIMV2", RemoteMachine), connOptions);
   managementScope.Connect();

   Log.LogMessage(string.Format(CultureInfo.CurrentCulture, "Connected to {0}: {1}",
   RemoteMachine, managementScope.IsConnected));

   if (!managementScope.IsConnected)
   return false;

   var objectGetOptions = new ObjectGetOptions();
   var managementPath = new ManagementPath("Win32_Process");
   var processClass = new ManagementClass(managementScope, managementPath, objectGetOptions);

   var processParameters = processClass.GetMethodParameters("Create");

   processParameters["CommandLine"] = Command;
   try
   {
     var outParams = processClass.InvokeMethod("Create", processParameters, null);
     var processID = Convert.ToUInt32(outParams["processId"]);

     Log.LogMessage(string.Format(CultureInfo.CurrentCulture,
       "Creation of the process {0} returned: {1}", processID, outParams["returnValue"]));

     var stopQuery = new WqlEventQuery(string.Format(CultureInfo.CurrentCulture,
       "select * from Win32_ProcessStopTrace where ProcessID={0}", processID));

     var processStopEvent = new ManagementEventWatcher(managementScope, stopQuery);
     processStopEvent.Options.Timeout = new TimeSpan(0, 1, 0);

     Log.LogMessage("Waiting for process to complete...");
     processStopEvent.WaitForNextEvent();
     processStopEvent.Stop();

     Log.LogMessage("Completed.");
   }
   catch (Exception ex)
   {
     Log.LogError(ex.Message);
   }

   return true;
   }
}

The task can then be used as follows:

<UsingTask TaskName="RemoteExec" AssemblyFile="BuildTasks.dll"/>
<RemoteExec RemoteMachine="$(Server)" Command="application.exe" />

There we have it, executing a command remotely from MsBuild!

Tagged with: ,

Creating JunctionPoints with MsBuild

Posted in team foundation server by Sydney du Plooy on October 10, 2009

With our build process we decided on a deployment folder structure that required us to create NTFS Junction Points. Junction Points are links to other folders, also known as soft links. Below is an example of how these links appear in Windows 7. It also shows our deployment folders for various build types:

JunctionPointFolderStructureAll the folders with shortcut arrows are junction points or links to folders in the Published folder. For each build, the binaries are copied to a build numbered folder in the Published folder. Only when a build is successful; meaning that all code compiled and unit tests passed; do we update the junction point to the relevant build numbered folder in Published.

By using the code written by Jeff Brown in this article, I was able to create a custom build task that enabled us to the create and delete junction points with relative ease. Here is a link to the source code for an MsBuild task that will enable you to manage junction points with MsBuild.

Tagged with: ,

Automating a ClickOnce Deployment – Part1

Posted in team foundation server by Sydney du Plooy on September 6, 2009

In our build process, using Team Foundation Build, we decided to use ClickOnce for application deployment. In this post I’ll try to set the lay of the land. In the following couple of posts I will take you through the process of setting up a clickonce deployment.

Choosing the deployment strategy

There are essentially two different ways to deploy clickonce applications. They are:

  1. Physical media such as CD or DVD
  2. Network share or web

Deciding on the strategy that will work for you is largely determined by the available bandwidth at the client site. If there is none to very little bandwidth, or no internet connectivity, choose CD / DVD. Otherwise choose Network share or web.

We opted for the web strategy and so I will base the series around this strategy.

Lay of the land

Assume that we have one build server and one deployment server. The build server will push the build artifacts to the deployment server from where clients will use clickonce to install the application. The application is pushed to the deployment server after every build with a new version number.

To make the explanations easier we will setup a hypothetical configuration and base the deployment configuration on it. Our hypothetical solution consists of a single application, which we will call App. Below is a diagram that demonstrates this configuration:

Note: To hide the published share simply name it with a trailing dollar sign i.e. “Published$”.

Initial configuration

First of all we need a folder on the deployment server to push the build artifacts to. For this, create a published folder on the deployment server and share it with modify permissions granted to the TFSSERVICE account. Remember the TFSSERVICE account that TFS uses for the build service?

Furthermore, we need to configure Internet Information Services (IIS) to interpret the clickonce files correctly according to their extension. For this, add the following MIME types in IIS:

.application    application/x-ms-application
.manifest       application/x-ms-manifest
.deploy         application/octet-stream

If you are using IIS 6.0, have a look at this article “IIS 6.0 Does Not Serve Unknown MIME Types”.

In the next installment I’ll discuss the different files that describe the application and deployment.

Tagged with: , ,

CopyDirectoryDifferences Build Task

Posted in team foundation server by Sydney du Plooy on September 1, 2008

In my attempt to optimize our build script work, I found myself needing to copy a list of files from one directory to another without overwriting existing files in the destination directory.

Scenario:

We need to gather code coverage information using multiple test containers (assemblies with unit tests inside them). This results in instrumenting all the assemblies that the test container depends on. This happens before the execution of unit tests in each test container.

Solution:

Use the MSBuild copy task to copy all the files from the source directory to the destination directory without overwriting the files that already exists in the destination directory. Simple enough, eh? Except, there is no support for this scenario to be found in the Copy Task that ships with MSBuild by default.

We are left no choice but to roll our own. Here is a code listing of the task that I wrote to do precisely that:

/// <summary>
/// Copies files from a source directory to a destination directory that
/// do not exist in the destination directory.
/// </summary>
public class CopyDirectoryDifference : Task
{
  private string _SourcePath = string.Empty;
  private string _DestinationPath = string.Empty;
  private ITaskItem[] _FilesCopied = {};

  /// <summary>
  /// Gets or sets the source path from where files will be copied.
  /// </summary>
  [Required]
  public string SourcePath
  {
    get { return _SourcePath; }
    set { _SourcePath = value; }
  }

  /// <summary>
  /// Gets or sets the destination path to where files will be copied.
  /// </summary>
  [Required]
  public string DestinationPath
  {
    get { return _DestinationPath; }
    set { _DestinationPath = value; }
  }

  /// <summary>
  /// Gets the files that were copied to the destination path.
  /// </summary>
  [Output]
  public ITaskItem[] FilesCopied
  {
    get { return _FilesCopied; }
  }

  /// <summary>
  /// Finds the files that are different between two directories and copies them from
  /// the source directory to the target directory.
  /// </summary>
  /// <returns>true if the task completed successfully, otherwise false.</returns>
  public override bool Execute()
  {
    if (!Directory.Exists(_SourcePath))
    {
      Log.LogError("Source path does not exist.");
      return false;
    }

    if (!Directory.Exists(_DestinationPath))
    {
      Log.LogError("Destination path does not exist.");
      return false;
    }

    string[] sourceFiles = Directory.GetFiles(_SourcePath);
    string[] destinationFiles = Directory.GetFiles(_DestinationPath);
    List<itaskitem> copiedFiles = new List</itaskitem><itaskitem>();

    foreach (string file in sourceFiles)
    {
      string fileName = Path.GetFileName(file);

      if (!File.Exists(Path.Combine(_DestinationPath, fileName)))
      {
        Log.LogMessage("Copying file {0} from {1} to {2}.", fileName, _SourcePath, _DestinationPath);

	try
	{
	  File.Copy(Path.Combine(_SourcePath, fileName), Path.Combine(_DestinationPath, fileName), false);
	  copiedFiles.Add(new TaskItem(Path.Combine(_SourcePath, fileName)));
	}
	catch (IOException ex)
	{
	  Log.LogError("Failed to copy the file {0}. Exception: {1}", Path.Combine(_SourcePath, fileName), ex.ToString());
	  return false;
	}
      }
    }

    if(copiedFiles.Count > 0)
      _FilesCopied = copiedFiles.ToArray();

    Log.LogMessage("Done.");

    return true;
  }
}

Tagged with: , ,