Over the course of the past week(!), I’ve been attempting to deploy and install a Windows Service using MSBuild (Specifically as driven by TFS, but it’s all MSBuild under the covers). This blog post details what I’ve tried, what didn’t work, and what the problem is (and how I solved it). It also details all of my research on the topic.
If you can see something I’m missing, please leave a comment. For the rest of you, consider this guilt free schadenfreude.
Here are the constraints:
- Deploy a Windows Service to a Windows Server joined to the Domain (Same domain as the Build Server)
- Ensure that the service is created (if it doesn’t exist), and started after the deployment process is complete.
- Able to deploy through TFS using MSBuild
- The Windows Service is kept in a standard directory on the target server (for our purposes, let’s say it’s E:\Services, or UNC Path: \\DeployedServer01\E$\Services
- Do not change the existing code for the Windows Service (e.g., put it in an installer, or make it self installing).
The problem I’m running into is “destination directory is null or cannot be accessed.”
Here’s what I’ve done so far.
I also have the .bat files necessary to create, delete, stop, and start the service that are listed in this gist.
I used the following MSBuild command to run the build (actually, I ran it through TFS, but if I were running it from the command line, it’d look like this):
msbuild deploy.proj /p:DeploymentServer=”DeployedServer01″ /p:DeploymentServerFolder=”\\DeployedServer01\E$\Services”
Unfortunately, this did not work. The files simply would not copy to the output folder. My next step was to determine why; to do so I needed to know who MSBuild was building the project as.
To debug this issue, I found this neat set of debugging targets for MSBuild:
<Target Name="DebuggingTFSBuild"> <Message Text=" MSBuildProjectDirectory = $(MSBuildProjectDirectory)" /> <Message Text=" MSBuildProjectFile = $(MSBuildProjectFile)" /> <Message Text=" MSBuildProjectExtension = $(MSBuildProjectExtension)" /> <Message Text=" MSBuildProjectFullPath = $(MSBuildProjectFullPath)" /> <Message Text=" MSBuildProjectName = $(MSBuildProjectName)" /> <Message Text=" MSBuildBinPath = $(MSBuildBinPath)" /> <Message Text=" MSBuildProjectDefaultTargets = $(MSBuildProjectDefaultTargets)" /> <Message Text=" MSBuildExtensionsPath = $(MSBuildExtensionsPath)" /> <Message Text=" MSBuildStartupDirectory = $(MSBuildStartupDirectory)" /> <Message Text=" " /> <Message Text=" " /> <Message Text=" Environment (SET) Variables* " /> <Message Text=" --------------------------- " /> <Message Text=" COMPUTERNAME = *$(COMPUTERNAME)* " /> <Message Text=" USERDNSDOMAIN = *$(USERDNSDOMAIN)* " /> <Message Text=" USERDOMAIN = *$(USERDOMAIN)* " /> <Message Text=" USERNAME = *$(USERNAME)* " /> </Target>
I invoked them using:
msbuild deploy.proj /DebuggingTFSBuild
From this, I found out that a Domain user account had been set up to run the build. I then gave that user “Full Control” permissions on the share.
Running the build again, it again failed.
So I did what anyone would do, I escalated. I then added that account to the “Local Administrators” group for that computer. Re-ran the build, and it failed.
I spent some more time trying to debug this solution; specifically trying to use RoboCopy and XCopy in conjunction with this solution; both failed (permissions issues).
To be clear; here’s where I was at at this stage:
- Was able to successfully Delete, Create, Stop, or Start the service during the course of the build (as outlined in the above solution)
- Was able to have TFS ‘drop’ the executable and related files to the target server (a drop location in the same share, but different directory)
- was not able to copy the ‘drop’ folder’s files to the static service location
On to solution #2 (or Solution #4, depending on the permutations) – Copy files to Fixed Location using TFS.
Of course, this didn’t work either. I received the error “TF270001: Failed to copy. The destination directory is null or cannot be accessed.” Have you ever tried a Google search for this issue?
Of course, TFS doesn’t provide any easy way to check the directories, so I had to output the values of the variables into the MSBuild log (this can also be done through an Activity Message in the TFS Template builder).
After double (and triple, and quadruple) checking the source and destination directories, I scrapped this approach, and followed the advice in this Stack Overflow post. At this point, I just wanted to get it to work, enter Solution #5: Hack it.
I used pieces of the original solution; but instead of relying on the agent’s account, I created a Domain Account that had permissions; and then created naked <Exec Command=””/> to allow me to run the commands needed to copy the files:
<Exec Command="IF NOT EXIST P: net use P: \\DeployedServer01\E$\Services /user:domain\deployUser passw0rd" /> <Exec Command="xcopy "$(OutDir)*.*" P:\* /y" /> <Exec Command="net use P: /delete /y" />
This solution works: It bypasses whatever Windows/Domain authentication issue the build agent user was having, and it ensures the files are copied to the fixed location.
- MSDeploy: I attempted a solution using MSDeploy, but some of the parameters didn’t make sense, so I didn’t spend much time on it (notably the IISDeployPath — why would I want such a parameter for a Windows Service?)
mklinkto symlink the drop location to the static path. This ran into UNC Path issues that were a pain in the ass (and ultimately unresolvable).
- Using InvokeProcess to try to CopyDirectory in TFS Build.
In the “Things that should be Apparent but aren’t” category:
- Why does TFS make it so hard to get the “Drop Location” directory?
$(OutDir)refers to the Build server’s build folder, *not* the drop location. Since we are on TFS 2012 (and not 2013), we can’t use any of the ‘nifty’ variables that are in 2013.
- Why isn’t dropping to a fixed location considered a standard deployment task?
- Why does the MSbuild
Copy Taskhave poor debugging? Even in Detailed verbosity, there was no information that was useful from that task.
- Windows Workflow and TFS Build Templates: Truly “Enterprise-y”
- Why is it so hard to debug authentication issues on TFS? Legitimately, once the user had permissions to those folders, it should have worked. I can only wonder if the user TFS said it was running as was not in fact the user it was running as (although that seems weird, since the build agent had permissions to the drop folder).
Miscellaneous notes on MSBuild and TFS Build:
- It appears multiple people have issues with the MSBuild Copy task. Jarrod Dixon reports that ItemGroups are parsed before targets, leading to counter-intuitive syntax for the Copy task.
- CopyDirectory has multiple bugs: Long paths (anything over 248 characters, and it only issues a warning (and not a build failure) when the source directory doesn’t exist.
- Obtaining the files that TFS Drops is harder than you think. You can get them from MSBuild if you know the magical incantation.
- Finding out what version of MSDeploy is on your server takes a little registry diving.
All in all, I spent 5 days, 70 commits, and 70+ builds to get this to work. I still don’t know why the ‘recommended’ solutions failed; or how to get MSDeploy to play nice with MSBuild and TFS for a Windows Service, but what I do know is this: It shouldn’t take this much effort to be able to deploy a Windows Service. It should be an ‘out of the box’ feature for TFS; along side more useful debugging when it doesn’t work.