18

I am using Web Deploy to package and deploy web sites for my product. In particular, I have two different projects in my solution I use this method to deploy.

I have a third project in the solution (a windows service) that also needs to be installed on the web server.

I know I can write a custom manifest (for the dirPath, filePath and runCommand providers) and directly call MsDeploy to deploy it. But I would like to leverage the existing MsBuild tasks to package my service if possible.

I see it is possible to do some customization of the manifest file via msbuild targets:

http://social.msdn.microsoft.com/Forums/en/msbuild/thread/1044058c-f762-456b-8a68-b0863027ce47

Particularly by using the MsDeploySourceManifest item.

After poking through the appropriate .targets files, it looks like either contentPath or iisApp will get appended to my manifest if I use the Package target. Ideally I'd just like to copy an assembly (or directory), possibly set ACLs, and execute installutil.exe on the service.

Is it possible to completely customize the manifest generated by the Package target, by editing my csproj file?

If not, is there a simple way to build a new target that will do the equivalent to Package, yet allow me to spit out a completely custom manifest?

Sayed Ibrahim Hashimi
  • 43,864
  • 17
  • 144
  • 178
Merlyn Morgan-Graham
  • 58,163
  • 16
  • 128
  • 183
  • Did you get an answer to this? – musica Oct 16 '11 at 17:20
  • @Graci: I did. I am finishing the work item for this during this week, and I'll post an answer after I have it all working. The key concepts are to create a custom `.targets` file, use your custom targets to add `runCommand` provider entries to execute custom batch files (for stopping/uninstalling the service, and installing/starting it), add a custom Parameters.xml file to the project, and set certain flags, such as `IncludeIisSettingsOnPublish=False` and `IncludeIisSettingsOnPublish=False`. Oh, and the free SlowCheetah non-web transform extension helped too. – Merlyn Morgan-Graham Oct 17 '11 at 02:50
  • @Merlyn: It would be great if you found time to share your solution, from your last comment it sound's like you've got it covered well. – Jakub Januszkiewicz Nov 21 '11 at 09:46
  • @JakubJanuszkiewicz: Check my new answer, and the link from it. Please let me know if you have questions - I'm totally up for improving this, though I don't want to make it sparkle like a diamond unless people are interested :) – Merlyn Morgan-Graham Nov 22 '11 at 06:30
  • @musica: I got an answer. Sorry it took me so long to get a write up together :) Check it out if you're still interested. – Merlyn Morgan-Graham Nov 23 '11 at 01:50
  • @SayedIbrahimHashimi: Thanks for taking the time to check out the answer and for fixing my tags :) Feel free to contribute more if you have more to add. – Merlyn Morgan-Graham May 21 '12 at 23:29
  • @MerlynMorgan-Graham it would be better to not import the Web .targets file (you're bringing in thousands of lines of MSBuild targets into you build process). But I think that would be tough w/o it because it you want; to be able to fully install the service including stop/start/register and you want to be able to execute via the generated .cmd file. How well is the technique below working for you? – Sayed Ibrahim Hashimi May 22 '12 at 07:02
  • @SayedIbrahimHashimi: I ended up getting this done right around the time we were done with the project, so I haven't had a lot of field testing of my solution. It works, but all in all it feels like the wrong tool for the job and is not very streamlined or bulletproof. Part of this may be due to my CI/CM factoring though. If I take this approach in the future, I'll probably treat the service as a completely separate component/project from the web site it backs. I might also wrap the service with an MSI. – Merlyn Morgan-Graham Sep 14 '12 at 03:48

1 Answers1

17

I don't have a complete write up yet for people trying to learn how this works, but I now have a write up for how to at least accomplish this goal.

http://thehappypath.net/2011/11/21/using-msdeploy-for-windows-services/

(edit: link is dead for now. Let me know if you're interested and I can post it elsewhere).

My guide goes through these overall steps:

  • Ensure the service starts itself upon installation (not crucial, but easier to deal with)
  • Add the Microsoft.WebApplication.targets file to your project, even though you don't have a web project. This enables the Package MsBuild target.
  • Add a custom .targets file to your project that builds a custom MsBuild package manifest
  • Add some batch scripts to your project to stop/uninstall and install the service
  • Add a Parameters.xml file to support changing the target deployment directory a bit more easily
  • Set up app.config transformations using the SlowCheetah Visual Studio addon

Then you can package your project with this command line:

msbuild MyProject.csproj /t:Package /p:Configuration=Debug

You can deploy the resulting package with this command line:

MyService.Deploy.cmd /Y /M:mywebserver -allowUntrusted

The most undocumented part of this (except for my guide) is creating the custom manifest. Here's a dump of my current file (note, it is still a bit buggy, but can be fixed - See this question: MsDeploy remoting executing manifest twice - and try to keep to using only direct batch files for runCommand).

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build"
         xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- This file must be included before Microsoft.Web.Publishing.targets so we can hook into BeforeAddIisSettingAndFileContentsToSourceManifest -->

  <PropertyGroup>

    <!-- Include our targets -->
    <IncludeStopServiceCommand>True</IncludeStopServiceCommand>
    <IncludeSetCustomAclsProvider>True</IncludeSetCustomAclsProvider>
    <IncludeInstallServiceCommand>True</IncludeInstallServiceCommand>
    <IncludeMoveAppConfigToCorrectPackagePath>True</IncludeMoveAppConfigToCorrectPackagePath>

    <!-- Uncomment to enable more verbose MsBuild logging -->
    <!-- <EnablePackageProcessLoggingAndAssert>True</EnablePackageProcessLoggingAndAssert> -->

    <!-- Enable web.config transform, but hack it to work for app.config -->
    <ProjectConfigFileName>app.config</ProjectConfigFileName>
    <TransformWebConfigEnabled>True</TransformWebConfigEnabled>
    <UseParameterizeToTransformWebConfig>True</UseParameterizeToTransformWebConfig>

    <!-- Enable web project packaging, but hack it to work for non-web app -->
    <DeployAsIisApp>False</DeployAsIisApp>
    <IncludeIisSettingsOnPublish>False</IncludeIisSettingsOnPublish>
    <IncludeSetAclProviderOnDestination>False</IncludeSetAclProviderOnDestination>
    <DisableAllVSGeneratedMSDeployParameter>True</DisableAllVSGeneratedMSDeployParameter>

    <!-- Insert our custom targets into correct places in build process -->
    <BeforeAddIisSettingAndFileContentsToSourceManifest Condition="'$(BeforeAddIisSettingAndFileContentsToSourceManifest)'==''">
      $(BeforeAddIisSettingAndFileContentsToSourceManifest);
      AddStopServiceCommand;
    </BeforeAddIisSettingAndFileContentsToSourceManifest>

    <AfterAddIisSettingAndFileContentsToSourceManifest Condition="'$(AfterAddIisSettingAndFileContentsToSourceManifest)'==''">
      $(AfterAddIisSettingAndFileContentsToSourceManifest);
      AddSetCustomAclsProvider;
      AddInstallServiceCommand;
    </AfterAddIisSettingAndFileContentsToSourceManifest>

    <OnAfterCopyAllFilesToSingleFolderForPackage Condition="'$(OnAfterCopyAllFilesToSingleFolderForPackage)'==''">
      $(OnAfterCopyAllFilesToSingleFolderForPackage);
      MoveAppConfigToCorrectPackagePath;
    </OnAfterCopyAllFilesToSingleFolderForPackage>

  </PropertyGroup>

  <!-- Custom targets -->
  <Target Name="AddStopServiceCommand" Condition="'$(IncludeStopServiceCommand)'=='true'">
    <Message Text="Adding runCommand to stop the running Service" />
    <ItemGroup>

      <MsDeploySourceManifest Include="runCommand">
        <path>$(_MSDeployDirPath_FullPath)\bin\servicestop.bat</path>
        <waitInterval>20000</waitInterval>
        <AdditionalProviderSettings>waitInterval</AdditionalProviderSettings>
      </MsDeploySourceManifest>

    </ItemGroup>
  </Target>

  <Target Name="AddSetCustomAclsProvider" Condition="'$(IncludeSetCustomAclsProvider)'=='true'">
    <ItemGroup>

      <MsDeploySourceManifest Include="setAcl">
        <Path>$(_MSDeployDirPath_FullPath)</Path>
        <setAclUser>LocalService</setAclUser>
        <setAclAccess>FullControl</setAclAccess> <!-- Todo: Reduce these permissions -->
        <setAclResourceType>Directory</setAclResourceType>
        <AdditionalProviderSettings>setAclUser;setAclAccess;setAclResourceType</AdditionalProviderSettings>
      </MsDeploySourceManifest>

    </ItemGroup>
  </Target>

  <Target Name="AddInstallServiceCommand" Condition="'$(IncludeInstallServiceCommand)'=='true'">
    <Message Text="Adding runCommand to install the Service" />
    <ItemGroup>

      <MsDeploySourceManifest Include="runCommand">
        <path>cmd.exe /c $(_MSDeployDirPath_FullPath)\bin\serviceinstall.bat</path>
        <waitInterval>20000</waitInterval>
        <dontUseCommandExe>false</dontUseCommandExe>
        <AdditionalProviderSettings>waitInterval;dontUseCommandExe</AdditionalProviderSettings>
      </MsDeploySourceManifest>

    </ItemGroup>
  </Target>

  <Target Name="MoveAppConfigToCorrectPackagePath"
          Condition="'$(IncludeMoveAppConfigToCorrectPackagePath)'=='true'">
    <PropertyGroup>
      <OriginalAppConfigFilename>$(_PackageTempDir)\App.Config</OriginalAppConfigFilename>
      <TargetAppConfigFilename>$(_PackageTempDir)\bin\$(TargetFileName).config</TargetAppConfigFilename>
    </PropertyGroup>

    <Copy SourceFiles="$(OriginalAppConfigFilename)" DestinationFiles="$(TargetAppConfigFilename)" 
          Condition="Exists($(OriginalAppConfigFilename))" />
    <Delete Files="$(OriginalAppConfigFilename)" 
            Condition="Exists($(OriginalAppConfigFilename))" />
  </Target>

</Project>
Community
  • 1
  • 1
Merlyn Morgan-Graham
  • 58,163
  • 16
  • 128
  • 183
  • Why are you including in the steps lists their original values, when earlier you are asserting that they are empty? Is it really neede due to something hideous? I mean, some of the properties look like ` $(prop); extension `, so they are both 'use if not set' and 'injecting, not overriding'. Your sample attaches it self only if nothing other is attached already, and at the same time it tries to preserve the original contents (assumed to be empty directly before it). I'd think that either that conditions, or the 'concat' behavior is not needed. – quetzalcoatl Jan 24 '14 at 15:30
  • Anyways, great work! I just came to a similar patches/extensions before finding your snippet, so probably that's the only way. Btw. I've also found the `TransformWebConfigEnabled` to have `true` by default, so probably it can be trimmed too. Btw2. have you tried parameterization of the app.config? Aren't there any problems with applying the values from `SetParameters.xml` due to the fact that the `webconfig/appconfig` was moved&renamed? – quetzalcoatl Jan 24 '14 at 15:38
  • Hm.. Yes, I'm quite sure there'll be problem, as there's clear reference to old 'App.config' in the `parameters.xml` inside the package. I think that some additional fixes are needed to `ImportParametersFiles` target, or near to that. – quetzalcoatl Jan 24 '14 at 16:07