:::: MENU ::::

Thursday, November 12, 2009

Many posts (e.g.: http://blog.deploymentengineering.com/2007/06/dealing-with-very-large-number-of-files.html, http://www.wintellect.com/cs/blogs/jrobbins/archive/2007/10/19/wix-the-pain-of-wix-part-2-of-3.aspx, http://blog.deploymentengineering.com/2007/06/burning-in-hell-with-heat.html) have been written about the problem of adding large number of files to a WIX installer. This problem is the most painful when you want to add content files that do not really have any special purpose, but just have to be there (e.g. code samples or source code packages).

I also struggled with this problem, and finally I found myself creating a small MsBuild tool (WixFolderInclude.targets) that you can include in your WIX project and use to generate file installers for entire folders on the disk. :-)I call it a tool, as I don’t have a better name for it, but it is not (only) an MsBuild target, nor is it a Task. Actually it is a WIX MsBuild extension, but WIX already has a “WIX extension” term, which is something else. So let’s stick to “tool”. 

The WixFolderInclude tool

Let’s see how you can use this tool; it was tested with the latest WIX framework (v3.0.4220), but it probably works with older v3x versions as well. I’m assuming that you are more or less familiar with the WIX and MsBuild concepts. If not, you can grab the necessary information quickly from Gábor Deák Jahn's WiX Tutorial and the MSDN docs.

WIX projects (.wixproj) are MsBuild projects, and you can extend them with other MsBuild property definitions or targets. One option is to modify the wixproj file in a text editor… This is fine, but I like the WIX project to open in Visual Studio, and in this case modifying the project file is not easy. Instead, I usually always start by creating a “Build.properties” file in the WIX project (it has type “Content” so it does not modify the WIX compilation), where I can write my MsBuild extensions. I have to modify the project file only once, when I include the Build.properties file. I usually include it directly before the wix.targets import:

</ItemGroup>
<Import Project="$(MSBuildProjectDirectory)\Build.properties" />
<Import Project="$(WixTargetsPath)" />

But you can directly write into the project file as well, if you don’t use the VS integration.

Let’s take a very simple example: I would like to include two code samples in the installer. They are located in some folder (C:\Temp\ConsoleApplication1 and C:\Temp\WebApplication1) and I would like to install them in a “Samples” folder inside the program files entry of my installed application. Of course both samples contain sub-folders that I also want to include.

To achieve that with my tool,

  • you have to define MsBuild project items that describe the aspects of the installation of these folders
  • you have to define some property to utilize my tool
  • the tool generates some temporary WIX fragment files during compilation (and includes them in the compilation), which contain the definition of the Directory/Component/File structure and a component group that gathers the components generated for the files in the directory structure.
  • you have to include references to the generated component groups in the installation features of your choice in the normal wxs files (e.g. Program.wxs).

So first, let’s create the folder descriptions for my sample. The tool searches for project items called “WixFolderInclude”, so we have to create such items for the folders we want to include:

<ItemGroup>
  <
ConsoleApplication1Files Include="C:\temp\ConsoleApplication1\**" />
  <
WixFolderInclude Include="ConsoleApplication1Folder">
    <
SourceFiles>@(ConsoleApplication1Files)</SourceFiles>
    <
RootPath>C:\temp\ConsoleApplication1</RootPath>
    <
ParentDirectoryRef>Dir_Samples</ParentDirectoryRef>
  </
WixFolderInclude>
</
ItemGroup>

<ItemGroup>
  <
WebApplication1Files Include="C:\temp\WebApplication1\**" Exclude="*.scc" />
  <
WixFolderInclude Include="WebApplication1Folder">
    <
SourceFiles>@(WebApplication1Files)</SourceFiles>
    <
RootPath>C:\temp\WebApplication1Files</RootPath>
    <
ParentDirectoryRef>Dir_Samples</ParentDirectoryRef>
  </
WixFolderInclude>
</
ItemGroup>

As you can see, you can define the set of files to be included with the standard possibilities of MsBuild, so you can include deep folder structures, exclude files, or even list the files one-by-one. In the example here I have excluded the source-control info files (*.scc) from the second sample.

In the WixFolderInclude items, you have to note the following things.

  • The main entry (ConsoleApplication1Folder and WebApplication1Folder) describes the name of the folder installation. The generated component group ID will be based on this name, so you can use any meaningful name here, not necessarily the folder name.
  • The “SourceFiles” metadata should contain the files to be included in this set (unfortunately, you cannot use wildcards here directly, so you have to create a separate item for them).
  • The “RootPath” metadata contains the folder root of the folder set to be included in the installer. This could also be derived from the source file set (by taking the common root folder), but I like to have it more explicit, like this.
  • The “ParentDirectoryRef” metadata specifies the ID of the <Directory>, where the folder should be included in the installer. Now I have created a directory (Dir_Samples) for the Samples folder in the program files, so I have specified that as parent.

As we are ready with the definition, the next step is to set up the tool. It is very simple; you just have to include the following lines in the Build.properties (or the project file):

<Import Project="$(MSBuildProjectDirectory)\Microsoft.Sdc.Tasks\Microsoft.Sdc.Common.tasks" />

<PropertyGroup>

<CustomAfterWixTargets>$(MSBuildProjectDirectory)\WixFolderInclude.targets</CustomAfterWixTargets>
</
PropertyGroup>

The value of the CustomAfterWixTargets should point to the tool file. If you have it in the project folder, you can use the setting above directly. Also note that the tool uses the Microsofr.Sdc.Tasks library (http://www.codeplex.com/sdctasks). I have tested it with the latest version (2.1.3071.0), but it might work with older versions as well. You should import the Microsoft.Sdc.Common.tasks file only once, so if you have already imported it in your project, you can skip that line.

Now we are done with the entries in the Build.properties, so let’s include the folders in the installer itself. As I have mentioned, the tool generates fragments that contain a component group for each included folder. The component group is named as follows: CG_WixFolderInclude-name. In our case, these are CG_ConsoleApplication1Folder and CG_WebApplication1Folder. So let’s include them in the main feature now:

<Product ...>
  ...

  <!-- setup the folder structure -->
  <
Directory Id="TARGETDIR" Name="SourceDir">
    <
Directory Id="ProgramFilesFolder">
      <
Directory Id="INSTALLLOCATION" Name="WixProject1">
        <
Directory Id="Dir_Samples" Name="Samples">
        </
Directory>
      </
Directory>
    </
Directory>
  </
Directory>

  <!-- include the generated component groups to the main feature -->
  <
Feature Id="ProductFeature" Title="WixProject1" Level="1">
    <
ComponentGroupRef Id="CG_ConsoleApplication1Folder"/>
    <
ComponentGroupRef Id="CG_WebApplication1Folder "/>
  </
Feature>
</
Product>

And that’s it. We are ready to compile!

Fine tuning

The tool supports some additional configuration options, mainly for debugging purposes: you can specify the folder where the temporary files are stored (by default, it is the value of %TMP% environment variable) and whether it should keep the temp files (by default, it deletes them after compilation). These settings can be overridden by including the following lines in the Build.properties.

<PropertyGroup>
  <
WixFolderIncludeTempDir>C:\Temp</WixFolderIncludeTempDir>
  <
WixFolderIncludeKeepTempFiles>true</WixFolderIncludeKeepTempFiles>
</
PropertyGroup>

Possible problems

Of course, life is not that easy... so you might encounter problems with using this tool as well. One is that it kills MsBuild’s up-to-date detection, so it will recompile the project even if nothing has changed. I think this could be solved by specifying some smart output tags on the target, but it is not easy, and usually I want to be sure that the installer package is fully recompiled anyway.

The other – probably more painful – problem is that you cannot include additional files from WIX to a subfolder of an included directory. We had this problem when we wanted to create a shortcut to the solution files of the installed samples. The problem is that since the IDs that the Sdc Fragment task generates are GUIDs, you have no chance of guessing what the subfolder’s ID was.

I have extended the WixFolderInclude.targets to support generating deterministic names for some selected folders. The folders to be selected can be defined with the “DeterministicFolders” metadata tag of the WixFolderInclude item. The value should be a semicolon-separated list of folders relative to the RootPath. Please note that as these are folders, you cannot really use MsBuild’s wildcard support, but you have to type these folder names manually. Let’s suppose that we have a Documentation folder inside the ConsoleApplication1 sample, which we might be able to extend from WIX later. We have to define this as the following:

<ItemGroup>
  <
ConsoleApplication1Files Include="C:\temp\ConsoleApplication1\**" />
  <
WixFolderInclude Include="ConsoleApplication1Folder">
    <
SourceFiles>@(ConsoleApplication1Files)</SourceFiles>
    <
RootPath>C:\temp\ConsoleApplication1</RootPath>
    <
ParentDirectoryRef>Dir_Samples</ParentDirectoryRef>
    <
DeterministicFolders>Documentation</DeterministicFolders>
  </WixFolderInclude>
</
ItemGroup>

As a result, the ID for the Documentation’s <Directory> element will be: Dir_ConsoleApplication1Folder_Documentation, so we can extend it from our Product.wxs:

<DirectoryRef Id="Dir_ConsoleApplication1Folder_Documentation">
  <
Component Id="C_AdditionalFile" Guid="5D8142C1-...">
    <
File Name="AdditionalFile.txt" Source="C:\Temp\AdditionalFile.txt" />
  </
Component>
</
DirectoryRef>
 

Attachment

In the attached ZIP file, you will find the WixFolderInclude.targets file, and also the sample that I have used here to demonstrate the features (without the silly ConsoleApplication1 and WebApplication1 folders). Feel free to use them!

More

Monday, November 9, 2009

IIS6 uses the maxRequestLength config setting under the system.web section to specify maximum file upload size with a default of 4 MB.  IIS7 uses the maxAllowedContentLength config setting under the system.webServer section to specify maximum file upload size with a default of 28.6 MB.  This is something to watch out for when migrating your web application from IIS6 to IIS7.  Read on for more information how I found out about this new config setting for IIS7….

I have migrated several sites from IIS6 to IIS7 without much problems.  One gotcha that I did get caught on is the new IIS7 config settings section (system.webServer) and those settings for specifying the maximum file size to be uploaded to the website.  After migrating a certain web application from IIS6 to IIS7 everything appeared fine until a few customers began complaining about issues when uploading files to the website… in particular these were large files around 50MB.

In IIS 6.0 there is a config setting (attribute) called maxRequestLength located under the httpRuntime section in system.web that you can use to specify the maximum allowed request length (in other words the maximum uploaded file size).  In IIS 6.0 the default is 4096 which is number of kilobytes allowed… so a 4MB file is the default file upload size under IIS 6.0. 

A 4MB is pretty small these days so it is quite common to need to override the default and put in a different value here.  For the web application that I was migrating to IIS7, we had increased the maximum file size to 200MB (and told our customers 200MB was the max upload too).  This is what the httpRuntime section was set to:

<system.web>
    <httpRuntime maxRequestLength="204800" executionTimeout="7200"/>

So we migrated the web application to IIS7, tested some large file uploads (we tested with 20MB files… note this for later) and everything seemed great.  After rolling the website out to our customers, a couple weeks post release we got a couple complaints about customers not being able to upload files.  Their files were about 50MB in size.

At first this was puzzling because we clearly had the config setting in place that indicated 200MB was the new limit (or so we thought) AND files larger than 4MB were allowed (we had tested 20MB files).  But we could easily reproduce the customer issue with their 50MB files failing.  So what was going on?

Eventually we tracked it down to IIS7 and the new config section called system.webServer.  We had known that httpHandlers in IIS7 were now to be specified in the system.webServer/handlers section but what we did not know (and did not find out until our customers ran into it) was that the maximum request length setting for IIS7 is also in a new location.  In IIS7 you specify the maximum size of a file to be uploaded to the website by using the maxAllowedContentLength setting (system.webServer/security/requestFiltering/requestLimits >> maxAllowedContentLength).

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="209715200" ></requestLimits>

Now why didn’t our tests with 20MB files show this?  Because in IIS7 the default value for the maxAllowedContentLength setting is 30000000 and that is in bytes: 30000000 bytes = 28.6 MB.  So in IIS7 the default had increased to 28 MB so we did not notice it since we were testing with only 20 MB files (and assuming the default was 4MB).  In the end we got this resolved fairly quickly and it showed an issue in our testing (we really should have been testing a 200MB file… the limits of what we tell our customers).

 

The ASP.NET Membership system has a static method built-in for this.  You can use the GeneratePassword static method from the Membership class to create a new password:

String strongPassword = System.Web.Security.Membership.GeneratePassword(8, 1);

From the MSDN documentation, the two parameters are:

  • length – Int32
    • The number of characters in the generated password. The length must be between 1 and 128 characters.
  • numberOfNonAlphanumericCharacters – Int32
    • The minimum number of punctuation characters in the generated password.

Also from the documentation: the generated password will contain alphanumeric characters and the following punctuation marks: !@#$%^&*()_-+=[{]};:<>|./?.

But also not included in the documentation is that the returned password will not be a “dangerous string”; in other words it won’t look like a block of script. 

The Membership.GeneratePassword checks the newly generated password string using an internal method called CrossSiteScriptingValidation.IsDangerousString() and will not return a password that does not pass this test.  It will just loop and continue to generate a new one until it is not considered a dangerous string.