Deploying Windows Service via MSBuild

There are multitude of ways a windows service can be deployed – using installutil utility, setup project, using WiX, custom installer, sc command and even manually using xcopy and sc command.

In this post, I’ll show how to automate these manual steps via MSBuild. One of the benefits of using MSBuild is that you can easily plug in this MSBuild project file as part of your regular Visual Studio builds or any Continous Integration server that supports MSBuild projects such as Team Foundation Server, CruiseControl.net etc.

Finally, I’ll also show you how to integrate this with Team Foundation Server’s build.

tl;dr If you’re fairly accustomed with MSBuild project files, you can skip the explanation below and jump straight to the script files here. If not, read on!

Getting started

Service metadata

To customize how the windows service appears in services management console, we need to specify some meta data.

<PropertyGroup Label="ServiceMetaData">
  <ServiceName>ShinyNewService</ServiceName>
  <ServiceDisplayName>Shiny New Service</ServiceDisplayName>
  <ServiceDescription>A shiny new service, that changes the world.</ServiceDescription>
</PropertyGroup>

These properties are fairly self descriptive. Essentially they map to these fields on the actual management console.

Services Managment Console

Note: You can set many other aspects of the service, such as the user account, password, dependencies, startup type and more via sc config command.

Deployment target

To help during development, I wanted the same script to be able to deploy to my local PC as well as a remote server.

Based on the target machine, this section sets up some more build task properties.

<Choose>
  <When Condition="'$(DeploymentServerName)' == ''">
    <PropertyGroup>
      <!-- You can choose any path here. -->
      <DeploymentFolder>C:\$(ServiceName)</DeploymentFolder>
    </PropertyGroup>
  </When>
  <Otherwise>
    <PropertyGroup>
      <!-- should be in \\serverName format-->
      <DeploymentServer Condition="'$(DeploymentServerName)' != ''">
        $(DeploymentServerName)
      </DeploymentServer>

      <DeploymentFolder>$(DeploymentServer)\C$\$(ServiceName)</DeploymentFolder>

      <!-- 4:5:4 => Planned: Application: Upgrade.
                   For more reason codes, run "sc stop" -->
      <DeploymentReason>4:5:4</DeploymentReason>
    </PropertyGroup>
  </Otherwise>
</Choose>

This is a simple if-else path in MSBuild’s terms. The variable DeploymentServerName is used to figure out if this is a local deployment or a remote deployment.

The path to DeploymentFolder is set based on whether this is a local or remote deployment. You can set it to any valid path of your liking.

DeploymentReason is completely optional. You can set it if you want to be extra nice or if your internal poilcy demands, but otherwise you can ignore it.

ReBuild the project

To deploy the service, we need to gather its binaries. Unlike web applications, where you can simply take contents of _PublishedWebsite folder, there is no way to get to the list of files for non-web applications.

To overcome that, we simply ReBuild the project and collect all outputs.

<Import Project="$(ProjectFile)"
        Condition="'$(ImportProjectFile)'=='true'" />

<Target Name="Rebuild"
        Condition="'$(ImportProjectFile)'=='true'"
        DependsOnTargets="$(BuildDependsOn)"
        Outputs="@(AllOutputs->'%(FullPath)')" >

  <CreateItem Include="$(OutputPath)\**\*">
    <Output ItemName="AllOutputs" TaskParameter="Include"/>
  </CreateItem>

  <Message Text="Custom build invoked!" Importance="high"/>

</Target>

Copying Outputs

Now that we have all the neccessary files needed to deploy the service, the next step is to copy those files over to the DeploymentFolder.

<Target Name="CopyOutputs">
  <MSBuild Projects="$(MSBuildProjectFullPath)"
           Properties="ImportProjectFile=true" Targets="Rebuild">
    <Output ItemName="ProjectOutputs" TaskParameter="TargetOutputs"/>
  </MSBuild>

  <Message Text="Stopping Service..." />

  <!-- 4:5:4 => Planned: Application: Upgrade -->
  <Exec Command="safeServiceStop $(ServiceName) $(DeploymentServer)
                 $(DeploymentReason)" ContinueOnError="true" />

  <Message Text="Copying files..." />
  <Copy SourceFiles="@(ProjectOutputs)"
        DestinationFolder="$(DeploymentFolder)"
        SkipUnchangedFiles="true"
        OverwriteReadOnlyFiles="true" />
</Target>

There are couple of caveats here. First, if the service was already deployed before, there is a good chance that the service maybe running. In that case, copy may fail as some files maybe locked. So we need to stop the service first.

We can stop the service via sc stop command, however sc stop command is asynchronous, so it will return immediately. In other words, it will not wait for the service to completely stop before returning.

Second, you may also encounter cases where the service is not installed or the server is offline.

The batch script safeServiceStop takes care of all these cases and ensures that service has completely stopped before returning.

We then proceed to copy the outputs to the DeploymentFolder.

Deploying the Service

<Target Name="DeployService">

  <Exec Command="safeServiceStop $(ServiceName) $(DeploymentServer) $(DeploymentReason)" />

  <Exec Command="safeServiceDelete $(ServiceName) $(DeploymentServer)"
        ContinueOnError="true" />

  <Exec Command="sc $(DeploymentServer) create $(ServiceName)
                    binPath= "$(ServiceExecutablePath)"
                    start= delayed-auto
                    displayName= "$(ServiceDisplayName)"" />

  <Exec Command="sc $(DeploymentServer) description $(ServiceName)
                     "$(ServiceDescription)"" />

  <Exec Command="safeServiceStart $(ServiceName) $(DeploymentServer) "
        ContinueOnError="true" />
</Target>

The final step is to install or re-install the service as a windows service. This target first stops the service, then deletes (equivalent to uninstall) the previous version, installs the newly deployed one, sets the metadata and then starts the service. The batch scripts safeServiceStop, safeServiceDelete, safeServiceStart all take care of the asynchronous nature of sc command.

Usage

To use the script:

  • Save deploy.proj, safeServiceStart.bat, safeServiceStop.bat, safeServiceDelete.bat files in the same folder
  • Open command prompt in admin mode
  • To deploy to local PC, run msbuild deploy.proj
  • To deploly to remote server, run msbuild deploy.proj \\.

Note: You must be an admin on the remote server to be able to deploy to it.

The final code can be found here: Deploy-Windows-Service-Via-MSBuild

Integrating with Team Foundation Server build

Integrating the build script with Team Foundation Server is pretty easy. Using the default.xaml template,

  • Add deploy.proj to Items to Build ► Projects to Build setting.
  • Under Advanced ► MSBuild Arguments, specify the deployment server name and other parameters if needed.
  • Finally, save the build and take it for spin!

Team Build definition

References

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s