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!
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.
Note: You can set many other aspects of the service, such as the user account, password, dependencies, startup type and more via
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>
Now that we have all the neccessary files needed to deploy the service, the next step is to copy those files over to the
<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
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
safeServiceStart all take care of the asynchronous nature of
To use the script:
safeServiceDelete.batfiles in the same folder
- Open command prompt in admin mode
- To deploy to local PC, run
- 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
Items to Build ► Projects to Buildsetting.
Advanced ► MSBuild Arguments, specify the deployment server name and other parameters if needed.
- Finally, save the build and take it for spin!
- MSBuild: How to get all generated outputs by Sayed Ibrahim Hashimi
- Batch scripts for starting/stopping a windows service by Erik Falksen