Home
About
Contact
Monday, August 16, 2010

In Part 1 I hadn’t decided if I was going to use Web Deploy as the base of this blog series or the PowerShell scripts I already had in production. I decided to give Web Deploy a chance. In the end I didn’t regret it, but I must admit it was not straight forward. Hopefully this post will make it a breeze for you :-)

Web Deploy With VS 2010 and TFS 2010

VS 2010 and TFS 2010 now comes with Web Deploy integration and works great for low to medium complex web apps. When I tried it out with my requirements I did not manage to get my solution to work so I reverted back to the command line version.

The documentation for this topic is also sparse, and it looks/feels unfinished. Here’s some resources if you’re going down that road:

The Web Deploy MSBuild schema: C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0\Web

The official doc: http://msdn.microsoft.com/en-us/library/dd394698.aspx

Web Deploy @PDC: http://www.microsoftpdc.com/2009/FT56

FAQ: http://blogs.msdn.com/b/aspnetue/archive/2010/03/05/automated-deployment-in-asp-net-4-frequently-asked-questions.aspx

MSBuild Web Deploy arguments + more resources: http://weblogs.asp.net/jdanforth/archive/2010/04/24/package-and-publish-web-sites-with-tfs-2010-build-server.aspx

THE commands

The two commands below solves all the requirements I listed in my previous post. I spent a great deal of time learning and studying Web Deploy in order to come up with this, so I hope you find it useful. It might not look too hard now, but nothing is when you have the solution in front of you :-) I’m always open for better ways if you find one though. If I could ask the Web Deploy team for a new feature, it would be to make the two commands below really simple.

The rest of this post will discuss these commands in detail. Text in blue can be replaced with your own values.

Create package:

msdeploy.exe 
	-verb:sync 
	-source:manifest=PackageManifest.xml
	-dest:package=Package.zip,encryptPassword="certificatePassword" 
	-enableLink:AppPoolExtension
	-disableLink:CertificateExtension 
	-disableLink:ContentExtension 
	-replace:objectName=httpCert,targetAttributeName=hash,replace=08bf3e051bd10cd8d89ac1a3ac431887886ed343
	-replace:objectName=logFile,targetAttributeName=directory,replace=C:\Web\Logs 
	-replace:objectName=virtualDirectory,targetAttributeName=physicalPath,match="C:\\WebDeployTemplateWebSites",replace="C:\Web" 
	-declareParam:name=HttpsBinding,kind=DestinationBinding,scope=webSiteName,match=.*:443: 
	-declareParam:name=HttpBinding,kind=DestinationBinding,scope=webSiteName,match=.*:80:
	-declareParam:name=AppPoolUsername,kind=DeploymentObjectAttribute,scope=processModel,match=processModel/@userName 
	-declareParam:name=AppPoolPassword,kind=DeploymentObjectAttribute,scope=processModel,match=processModel/@password

Install package:

msdeploy.exe 
	-verb:sync 
	-source:package=Package.zip,encryptPassword="certificatePassword" 
	-dest:manifest=InstallManifest.xml,computerName=FQDN,username="domain\user",password="domainUserPassword" 
	-setParam:name=HttpsBinding,value=10.0.0.21:443: 
	-setParam:name=HttpBinding,value=10.0.0.21:80:
	-setParam:name=AppPoolUsername,value=MyDomain\AppPoolServiceAccount
	-setParam:name=AppPoolPassword,value=MySecureAppPoolPassword

Note that the above commands are one-liners. I only structured them the way I did to make them readable.

Complex

Web Deploy was built for the purpose of deploying web applications, so at first it looked like the perfect solution. However, I soon realized I had to study it in great detail to make it do what I wanted. I’m talking days not hours. Time I was not eager to spend to replace something already working or just for the sake of this blog post (sorry guys :-)). The reason for not giving up on it entirely was because Web Deploy had the potential to simplify/replace my scripts, have fewer moving parts, and make the total solution easier to maintain.

Around the same time as I published Part 1 I tweeted my concern about Web Deploy’s complexity and the @wdeploy team contacted me. I sent them a long email describing my concerns and they promised to do “creative things in the future” to make it less complex. I’ve been in contact several times after that and they are very responsive and eager to get feedback on the product. I’m personally looking forward to see (and maybe help influence) how this product will evolve.

Web Deploy Command Options

Before diving into the inner workings of these commands, we need to know a bit more about the tool. Web Deploy have a lot of functionality and is extremely powerful. Here’s the command line syntax:

	msdeploy.exe -verb:<verbName>
	-source:<provider>[=<pathToProviderObject>
		[,<providerSetting>=<providerSettingValue>]]
	[-dest:<provider>[=<pathToProviderObject>
        	         [,<providerSetting>=<providerSettingValue>]]
	]
	[-<MSDeployOperationSetting> ...]

Doesn’t look too scary right? There is more to it though…

Verbs
The following verbs exist:

  • delete
  • dump
  • getDependencies
  • getParameters
  • getSystemInfo
  • sync

Providers
Let’s look at which providers it has:

These are of course only the built-in providers, then you can create your own or use 3rd party ones if you like. A quick scan through this list shows that it does a lot more that just web stuff, like COM objects, registry settings, certificates, gac, databases.. the lot.

Provider Settings
The providers again have a set of common provider settings:

  • authType
  • computerName
  • encryptPassword
  • getCredentials
  • ignoreErrors
  • includeAcls
  • password
  • storeCredentials
  • tempAgent
  • userName
  • wmsvc

MSDeployOperationSetting
From the documentation:

Web Deploy operation settings are non-provider specific command-line flags. They modify all of a Web Deploy operation.

  • allowUntrusted
  • declareParam
  • declareParamFile
  • dest
  • disableLink
  • disableRule
  • disableSkipDirective
  • enableLink
  • enableRule
  • enableSkipDirective
  • postSync
  • preSync
  • removeParam
  • replace
  • retryAttempts
  • retryInterval
  • setParam
  • setParamFile
  • showSecure
  • skip
  • source
  • useCheckSum
  • verb
  • verbose
  • whatif
  • xml
  • xpath

Manifest Provider
We now have an overview of the command line syntax of the tool, but there are a few other important aspects. The first one being the manifest provider. Most likely you want to use more than one provider in your command, and that’s exactly what this provider does. Here’s an example from the documentation:

<sitemanifest>
   <appHostConfig path="mySite" />
   <gacAssembly path="System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
   <comObject path="Microsoft.ApplicationHost.AdminManager" />
   <contentPath path="c:\source" />
   <regKey path="HKLM\Software\ODBC" />
</sitemanifest>

I would actually preferred having these on the command line instead. When I trigger my commands from a build, I’m inserting variables into the command anyways and I would be better of with the ONE command and not have to maintain the xml files as well. Actually I would like to have both options. In the command I would suggest something like this (which would adhere to the existing conventions):

-source:appHostConfig=mySite,gacAssembly=System.Web…,[…]=[…]

Link Extensions
Another one is the concept of link extensions, which can be enabled/disabled by using the enableLink and disableLink operations. One example drawn from the documentation is:

…if you specify -disableLink:ContentExtension on the command line, you can prevent content from being included in a sync operation. This enables you to synchronize two Web servers without moving any content.

Web Deploy Rules
Web Deploy rules exists to disable or enable built-in or custom rules (using e.g. the enableRule and disableRule operations) for the sync verb. This is however (naturally) only true if the isDefault attribute is set to true in the rule definitions. See here to find the built-in rules. By creating the file Msdeploy.exe.configsettings in %program files%\IIS\Microsoft Web Deploy folder, you can add custom rules.

Packages and Archives
From the docs:

The Web Deploy package and archiveDir features let you create a snapshot backup of your Web site or Web server into a .zip file or archive directory. In addition, the parameterization and manifest features let you customize the archive or package that you create. You can then use your package files to deploy Web sites and Web servers to other computers or computer locations.

What’s wrong?

That’s a lot of stuff to consume! In order to effectively use this tool for deployment, you have to learn most of these verbs, providers and operations, figure out how they work and how you can take advantage of them.

This is (in my opinion) what Greg Young have been talking about and expressed in the NDC Magazine article Failure (well worth the read), where the intent is lost in the system and the user is forced to “reverse engineer” it to make sense of it all.

I’m fully aware of the endless amount of user scenarios that exists, and I don’t expect the Web Deploy team to cover them all, but the most common ones would be nice. Like the one I’m showing in this post I would think would be quite common for auto deployment scenarios. The advanced configuration could still be available for the not-so-common scenarios. Instead I’m “forced” to spend a lot of time learning the inner workings of a complicated tool, which in the end create a lot of value. How valuable wouldn’t it be if I didn’t have to invest so much time in learning the tool?

I’m also aware of the new TFS 2010 integration that exists, which simplifies many of these tasks, but that solution lacks documentation and the advanced options are hard to get at.

Setting up Web Deploy for remote access

To be able to access a server remotely, Web Deploy must be given access to the server. Several options are available, but I’ll focus on the Web Deployment Agent Service, which requires administrator privileges to use.

The Agent Service requires Web Deploy to be installed on the target server. You can find the detailed installation instructions here: http://technet.microsoft.com/en-us/library/dd569030(WS.10).aspx

Basic Usage

Let’s look at some vanilla examples for deploying web stuff remotely:

msdeploy
  -verb:sync
  -source:contentpath=c:\data,
  -dest:contentpath=c:\data,
      computerName=Server2,
      username=admin,
      password=pass

This copies your local C:\data directory to the server Server2, and provide a username and password to get access to the remote server. This is done by using the sync verb and the contentpath provider. So we can push files, which is nice cause we don’t have to use FTP, BITS or similar.

Deployment Requirements 

We want to do a lot more than just copy files. Here’s a list extracted from my previous post of what I want to deploy to remote servers:

  • File content
  • Virtual Directories and Applications settings
  • AppPools settings
  • Certificates
  • Bindings
  • Log location settings
  • Virtual Directory file location settings

Packaging structure

My commands can be used to produce one package per web site multiplied by the number of environments you have (e.g. Dev, Test and Prod). So if you have 4 web sites and 3 environments, you create 12 packages.

To better understand why this is, here’s a list of the settings that are specific for each environment and common across all servers in one specific environment:

  • File content
  • SSL Certificate password
  • Certificate hash/thumbprint
  • Web site log directory location
  • Virtual directory root locations

For server specific differences we create the package so that it accepts parameters for:

  • User and password for the service account under which the worker process of the Application Pools runs
  • The https binding (if used)
  • The http binding (if used)

If we did not define the last three parameters, we would have to multiply the packages with the amount of servers in the different environments as well. 4 web sites, 3 environments and 2 servers in each environment = 24 packages. You don’t want to go there…

Another option would be to define all possible changes as parameters, leaving you with only one package. I tried that. I could not figure out how to accomplish all of these using params, so I analyzed what I needed and used a little of both. It works, but could be better.

As an example of the above, a package for the web site blog.torresdal.net could be created for e.g. Dev with these settings:

  • File content = E:\MyBuildOutput\blog.torresdal.net
  • Certificate password = MySecureSSLPassword
  • Certificate hash=08bf3e051bd10cd8d89ac1a3ac431887886ed343
  • Web site log dir = E:\Logs\blog.torresdal.net
  • Virtual Directory locations = E:\Web

…and used these parameters when deploying:

  • Username for AppPool = MyDomain\AppPoolServiceAccount
  • Password for AppPool = MySecureAppPoolPassword
  • Https binding = 10.0.0.21:443:
  • Http binding = 10.0.0.21:80:

Notes on Application Pool

Note on bindings: You have to use either http or https or both. If you want to use hostname you could do: *:80:blog.torresdal.net
Note that host names are not supported on SSL in IIS 7, even though it’s technically possible to do.

Note on the service account for the application pool: On Windows Server 2008, IIS support Application Pool Identities. These are Windows virtual accounts that are assigned to the application pool effectively isolating it from other services. If you want to use these, then you don’t have to declare the parameters for the app pool identities as long as your source is using App Pool Id’s.

Dissecting The Create Package Command

I believe you now have an idea of what the commands above do, but lets pull it apart and describe each piece by itself.

The package manifest file
First we’re going to look at the manifest file which contains all source providers used to create the package:

<sitemanifest>
   <appHostConfig path="blog.torresdal.net" />
   <cert path="my\08bf3e051bd10cd8d89ac1a3ac431887886ed343 " />
   <dirPath path="E:\MyBuildDrop\LatestVersion\Dev\blog.torresdal.net" />
</sitemanifest>

This is the way that Web Deploy allows you to use more than one provider. Here’s a short description of each provider and what it does:

appHostConfig Gets all IIS specific settings from a web site.
cert Gets the certificate with the given thumbprint/hash.
dirPath Gets the content from the given path.

The create Package command
Now lets look at each section of the command:

-verb:sync Tells Web Deploy to do a sync
-source:manifest=PackageManifest.xml Points to the manifest file showed above containing all providers to use as source.
-dest:package=Package.zip,
encryptPassword="certificatePassword"
Use the package provider as destination for creating a package (zip) containing all content and information needed to deploy a package to a server. Since we’re using the cert provider (in the manifest) we need to provide the password to get access to the certificate.
-enableLink:AppPoolExtension I want the application pool to be synced as well.
-disableLink:CertificateExtension The appHostConfig provider include certificate and content by default. I want to control which certificate and what content to include, so I use –disableLink to disable these extensions. That is why I’ve added the cert and dirPath providers in the manifest so that I can be explicit about these.
-disableLink:ContentExtension See previous…
-replace:objectName=httpCert,
targetAttributeName=hash,
replace=08bf3e051bd10cd…
The appHostProvider and the cert provider are unaware of each others actions, so the appHostProvider outputs the thumbprint from the certificate found in IIS. I need to replace the httpCert hash property with the same hash used in the manifest file or else the web site would be bounded to the wrong certificate.
-replace:objectName=logFile,
targetAttributeName=directory,
replace=C:\Web\Logs
I want to replace the IIS log directory to a different path than the IIS I’m exporting from.
-replace:objectName=virtualDirectory,
targetAttributeName=physicalPath,
match="C:\\WebDeployMasterWebSites",
replace="C:\Web"
The web site I’m exporting from reside in C:\WebDeployMasterWebSites\{webSiteName}, and all Virtual Directories are located below this path. I want to control the root path, so I replace it.

Note: This would be a natural candidate for a parameter, but with params you can only replace the complete value, not just part of it.

Here’s a description of the parameter declarations allowing us to pass in params when deployment the package (these are also part of the same command):

-declareParam:name=HttpsBinding,
kind=DestinationBinding,
scope=webSiteName,
match=.*:443:
I want to control the SSL binding for the web site.
-declareParam:name=HttpBinding,
kind=DestinationBinding,
scope=webSiteName,
match=.*:80:
Same as above only for HTTP.
-declareParam:name=AppPoolUsername,
kind=DeploymentObjectAttribute,
scope=processModel,
match=processModel/@userName
I want to set the user name of the account under which the worker process of the Application Pools runs
-declareParam:name=AppPoolPassword,
kind=DeploymentObjectAttribute,
scope=processModel,
match=processModel/@password
Same as above only for the password.

To shorten the above command a tiny bit you can define the parameters in an xml file instead, like this:

<parameters>
   <parameter name="HttpsBinding" description="Web Site Binding for SSL">
      <parameterEntry kind="DestinationBinding" scope="sikker.frende.no" match=".*:443:" />
   </parameter>
   <parameter name="HttpBinding" description="Web Site Binding for http">
      <parameterEntry kind="DestinationBinding" scope="sikker.frende.no" match=".*:443:" />
   </parameter>
   <parameter name="AppPoolUsername" description="Account username for this application pool">
      <parameterEntry kind="DeploymentObjectAttribute" scope="processModel" match="processModel/@userName"/>
   </parameter>
   <parameter name="AppPoolPassword" description="Account password for this application pool">
      <parameterEntry kind="DeploymentObjectAttribute" scope="processModel" match="processModel/@password"/>
   </parameter>
</parameters>

You will then replace the –declareParam operation settings above with this:

-declareParamFile:MyParamFile.xml

However, if you’re going to execute this command from TFS or some other build tool, you’re better off leaving them inside the command. Less moving parts.

Dissecting The Deploy Package Command

We also need a manifest file when installing the package. This is ALMOST identical to the manifest we used when creating the package:

<sitemanifest>
   <appHostConfig path="blog.torresdal.net" />
   <cert path="my\08bf3e051bd10cd8d89ac1a3ac431887886ed343 " />
   <dirPath path="E:\Web" />
</sitemanifest>

The only difference between the two is in the dirPath provider. For the package we used files from E:\MyBuildDrop\LatestVersion\Dev\blog.torresdal.net, but when deploying we PUT files to E:\Web.

The deployment package command
Each section in detail:

-verb:sync Tells Web Deploy to do a sync
-source:package=Package.zip,
encryptPassword="certificatePassword"
Uses the earlier created package as source. Since this package contains a certificate, you need to provide the certificate password in order to get access to it.
-dest:manifest=InstallManifest.xml,
computerName=FQCN,
username="domain\user",
password="domainUserPassword"
Points to the manifest file to use for deployment, and to which server we are deploying to with the username/password.
-setParam:name=HttpsBinding,
value=10.0.0.21:443:
Set the SSL binding for the web site. Using IP 10.0.0.21 on port 443 (SSL).
-setParam:name=HttpBinding,
value=10.0.0.21:80:
Same as above only for HTTP.
-setParam:name=AppPoolUsername,
value=MyDomain\AppPoolServiceAccount
Set the user name for the account under which the worker process of the Application Pools runs
-setParam:name=AppPoolPassword,
value=MySecureAppPoolPassword
Same as above, only for the password

The Build/Deploy Process Using Web Deploy

Here’s the exact same deployment process overview as in my previous post, only now with Web Deploy. As a result the process got two steps shorter :-)

image

What’s Next?

In my next post I’ll either show you how to control the load balancer during deployment or integration with TFS. Both will be covered in the end.

.Net | ASP.NET | Deployment | IIS | Tools | WebDeploy
Monday, August 16, 2010 7:21:00 AM (W. Europe Daylight Time, UTC+02:00)
Thursday, August 05, 2010

Update: Part 2 is now available, covering Web Deploy (a.k.a. msdeploy)

Getting code deployed should be as easy as open up a web site (only taking slightly longer :-)). Devs or IT people should not spend manual labor (with the possibility of mishaps) on getting files from one place to another, making changes to IIS (or whatever you’re using), restarting servers, copy/changing web.config files etc. That’s the job for scripts and automation tools. Not to mention the cost savings of not needing IT people to do deployment. I bet you there’s no one manual step in the process of deployment that cannot be automated. Saying that, you have to consider how many times you deploy per day/week/month or year before going for full automation. However, if you’re serious about being Agile/Lean, you can’t do without a auto deployment scheme.

In the coming blog posts I’ll walk you through the steps I went through to automate our deployment, and hopefully you’ll find it interesting and even suggest improvements.

Overview
Below is an overview of the environments we’re deploying to:

image

One Load Balancer (LB) in each environment, two web servers in Dev and Test, and 3 in Prod. The actual numbers might or might not be true ;-), but that doesn’t really matter. In addition there’s SQL Servers, but I will not cover that here.

Why have a LB in Dev? Reason number one is to catch any possible LB issues in Dev before going to Test and Prod, and have the exact same environment in Dev as in Prod. It’s also useful to try out new stuff, like having the LB do caching etc.

Deployment Frequency
For Dev we auto deploy every night (part of nightly build) and at will during day. For Test, 2-4 times per week and to Prod 1-2 times every 2nd week. That was yesterday! :-) Today we can do it when the sun comes out of the clouds (not often in Bergen), every time I refill my coffee cup or whenever we feel like. The point being: we are no longer constrained by how often we can deploy.

Why All These Environments?
You can read about that here, but for us:

  • Dev is where we try out things without physically hurting users, but still being in a real server environment avoiding the “works on my machine” issue.
  • Test is as close to Prod as we can get (at external hosting provider, different network, firewalls etc) and where we make sure things run smoothly before going to Prod.
  • Prod is Prod

Tech Details
Here’s the tech stuff we use which might be relevant:

  • All servers are running Windows Server 2008 R2
  • Web servers are running on IIS 7.5 (since we’re on R2)
  • Application Request Routing in IIS is used as Load Balancer and runs on 2008 R2 Server Core (if you like, check out my previous post about setting up and configuring ARR)
  • TFS 2010 for builds
  • Team City for CI

Also note that we have access to the actual subnet where Test and Prod lives. This does however not mean we have access to all servers and features in all environments, it just means we can be given access to certain things not recommended through external firewalls, like PowerShell Remoting. This is where your environments might be different from ours.

Some General Advice

Consider Using a LB Even If You Don’t Need One For Performance Reasons
Load Balancers are useful for other things than load balancing. The biggest benefit (except from its core task), is that you can do upgrades and maintenance on servers without taking the whole site offline, by always leaving at least one server online.

Consider Turning Off IIS Recycling
Do you know that IIS automatically recycle your applications every 1740 minute, effectively restarting them? Are your web sites free from memory leaks or do you want to know if you have memory leaks? Why not turn off recycling? This is too big of a topic to cover here, but go Google: IIS7 recycle.

Consider Using Windows Server 2008 R2 Server CORE 
This should get you slightly better performance, but for me it’s more about scripting. Most of the things that needs to be done on server core, must be performed from command line, forcing you to create scripts.

Why Is Deployment Difficult?
First of all because every environment is different and there are no really good tools to automate the whole process. The challenge is to find the right tools to solve the problems your organization is facing, and have the tools work for you to get to the final goal.

What’s The Challenges?
For us it was about:

  1. How can we safely move files from a build server to Dev, Test and Prod?
  2. How can we automate the process of taking a node out of an LB cluster?
  3. How can we safely execute an upgrade on a server in Dev, Test or Prod and get feedback of progress, errors, and abort and roll back on failure?
  4. How can we remotely make changes to IIS?
  5. How can we avoid all manual tasks? (like adding a virtual directory in IIS or copy a web.config file)

Safety and automation is two keywords that sticks out. Where safe means no-one else than the intended persons or services should be able to perform the specified tasks. Automation meaning no manual operations should ever be needed in either Dev, Test or Prod except from IT maintenance like hardware upgrades, windows update etc.

What tool options have we?

Copy files:

  • Secure FTP in IIS 7 on a non public/available IP
  • or PowerShell with BITS
  • or WebDeploy

Taking LB nodes offline/online:

  • Use PowerShell Remoting to execute PowerShell scripts on ARR server
  • or the Web Farm Framework

Safely execute an upgrade:

  • Use PowerShell Remoting to execute PowerShell scripts on web servers
  • or WebDeploy

Avoid manual tasks:

  • Script all tasks, so they can be repeated

The Build/Deploy Process     
image

What About MSI’s?
If you read my blog you know I’ve done quite a bit of MS Installer stuff and WiX in particular. MSI’s are perfect for deploying to multiple places where you have no control. The drawback is that most developers don’t know how to customize MSI’s and often end up with a versioning problem and leaving lots of old stuff behind on the server after upgrades. If you have people skilled in Windows Installer, please feel free to use MSI, but I personally find XCopy to be very easy and is what I recommend if you’re not an ISV. With MSI’s you still have to install them remotely, which could be done with WMI or PowerShell.

Notes on WebDeploy
I’m currently looking at using Web Deploy to simplify/reduce the amount of scripts needed. WebDeploy would replace the FTP and deploy steps, but first impression is that it’s too generic, making it really hard to do simple things without spending quite a bit of time learning the tool, it’s underlying package schema and IIS schemas. Hopefully one day Web Deploy will be the only tool I’ll need to execute the whole deployment process.

What’s Coming?
In future blog posts I’ll walk you through step-by-step how to accomplish the above solution. While I’m writing this I’m not 100% sure if it will be a solution using PowerShell (which I have in production) or a slightly modified version using Web Deploy. It all depends on which one is easiest and which has the potential of being maintained by other people than me in the long run.

Hopefully this will give you the input you need to fully automate your deployment process as well.

Agile | ASP.NET | Deployment | IIS | Lean | Tools
Thursday, August 05, 2010 8:30:29 AM (W. Europe Daylight Time, UTC+02:00)
Saturday, July 18, 2009

A bit late announcement from me, but 4th of July WiX version 3.0 was declared stable. You can also find updated documentation for v.3.

Regarding my focus on WiX lately I haven't done much in WiX for a while. I was planning to add some more posts to my WiX tutorials, but currently this has not been my priority after I changed job. I used to work for an ISV where deployment was very important. Where I currently work we're in full control of all servers we deploy to. I do however plan to do some stuff with WiX here as well, but that will have to be later.

Saturday, July 18, 2009 12:52:39 PM (W. Europe Daylight Time, UTC+02:00)
Thursday, January 15, 2009

A little self promotion :-) Back in September last year, Einar Ingebrigtsen interviewed me about Agile, CI and other stuff I’ve been working on at Contiki. You can check out the podcast here: http://www.ingebrigtsen.info/post/2009/01/15/3cAgile-talk-with-Jon-Aril-Tc3b8rresdal-3e.aspx

Thanks Einar for doing this. Was very strange hearing myself talk. Don’t think I’ve done that since elementary school :-)

Thursday, January 15, 2009 8:15:04 PM (W. Europe Standard Time, UTC+01:00)
Wednesday, December 10, 2008

Heath Stewart announced today that WiX 3.0 is officially in Beta. So what does that mean? Haven’t it been in Beta for a long time? Rob Mensching has this to say about that on his blog:

In this case the Beta release marks the turning point where all of the major features for this release are finished and the bug flow such is under control and on a solid downward trend.

That’s good to know. For me though I’ve found WiX 3.0 to be quite stable for a long time. Yes, I’ve found bugs, but not any that have scared me so that I’ve consider not using it for production. Yes, for production. Actually I’ve found WiX to be more stable than many of the existing commercial deployment products already out there. Not naming any names :-)

So are you planning to or already playing with WiX? Go get the latest version (3.0.4805.0): http://sourceforge.net/project/showfiles.php?group_id=105970&package_id=168888

Wednesday, December 10, 2008 10:58:40 PM (W. Europe Standard Time, UTC+01:00)
Sunday, October 26, 2008

This is my fifth post in my WiX and DTF series. Here are some others I’ve written:

After working quite a bit with Custom Actions (CA’s) in managed code I thought it was about time to show how we can debug CA’s. Before DTF it was almost impossible to step into your code written in C++, VB Script or Java Script and debug. With DTF this is simple, but still not 100% intuitive or straight forward. You have two options mentioned in the DTF documentation for debugging your code and a third which is not mentioned:

  1. Use environment variable MMsiBreak (not to be confused with MsiBreak)
  2. Attach the debugger to the process via a message box
  3. Use System.Diagnostics.Debugger.Launch

Using MMsiBreak

Here’s how to add MMsiBreak to your environment variables in Vista:

  1. Start –> Right click Computer –> Properties (or navigate to Control Panel –> System)
  2. Select Advanced system settings
  3. Click Environment Variables…
  4. In the System variables section click New…
  5. Set Variable name = MMsiBreak
  6. Set Variable value = Name of your custom action method

On my computer it looks like this:

MMsiBreakScreenShot

In my example it will break for debugging when Windows Installer is executing the CA GetWebSites. When that happens you will get the following dialog:

MMsiBreakScreenShot2

Select Yes and you will be prompted to select a debugger to use. If you already have your solution open in VS, you will get the option of using that. Note however that on Vista admin privileges is required, so you need to have your Visual Studio running as admin in order for it to bee listed in the below screen:

MMsiBreakScreenShot3

When I select the running VS (as above I’ve done above) I get something in VS which I can’t quite explain. The first time this happened to me I thought it didn’t work. I get this message:

MMsiBreakScreenShot4

The thing is that the source code is of course already displayed, but I thought there was some problems with loading the symbols. However, if you just click Ok and hit F5 (Start debugging) in VS, your breakpoint will hit. Just a thing to be aware of if you get this.

Using Message Box

By using a message box you can accomplish the same thing, but this requires changes to your code and a recompile of both your CA project and you WiX project. Personally I prefer the above method, but it’s always nice to have options :-)

In order to debug using message box you must:

  1. Make sure you have a reference to System.Windows.Forms.dll.
  2. Somewhere in your code (where you want to break) add some code for displaying your message box. Something like this:
    MessageBox.Show("Please attach a debugger.");
  3. Add a breakpoint somewhere in your code below your message box code
  4. Rebuild your CA project as well as you WiX project to get the new changes.
  5. Run your MSI.
  6. When the message box displays, go to Tools –> Attach to Process… (Ctrl + Alt + P)
  7. Find the process named rundll32.exe and attach.
  8. Click Ok on the message box in you installation.
  9. The breakpoint you set earlier should now be activated in VS

Using Debugger.Launch()

This is exactly the same as using MMsiBreak except you trigger it from code instead of a environment variable. Just add this line to your code where you want to debug:

System.Diagnostics.Debugger.Launch();

This will give you the same “Unhandled exception” dialog as before and the rest is the same.

The MSI log file

If you’ve been working with MSI’s you know this already, but I think this is something every developer/it-expert should know about and in my experience most people don’t. Sometimes when you run an MSI and get an error that maybe causes a rollback and you can’t get the installation to install, you’re stuck with a cryptic error message. If you’re lucky you can Google it and maybe find the problem (or solution), but often this is not the case. Then this little command might come in handy:

msiexec /i NameOfMSI.msi /l*v C:\Temp\install.log

The above command uses msiexec.exe which is what Windows uses when you double click on an msi. Actually Windows does exactly like the above except from the /l*v part and that’s where you tell msiexec to log to a file. Here’s what it means:

/l = Log
* = Everything
v = Verbose

You can skip the v and you will get slightly smaller log file. Personally I use v all the time.

Sunday, October 26, 2008 7:33:58 PM (W. Europe Standard Time, UTC+01:00)
Friday, October 24, 2008

This is my fourth post in my WiX and DTF series. Here are some others I’ve written:

Intro

In this article I’ll cover how to allow a user to choose which web site to install an application to by listing available web sites in a list box in the MSI installation wizard. As mention in my previous articles I'm applying best practices (at the best of my knowledge) to have the installer pass the Vista certification, so hopefully that will be the case if you use any of this in production :-)

When doing lookups in IIS using custom actions in the InstallUISequence (as we need to do in order to get available web sites), elevated privileges is required (at least in Vista SP1). This means we need the installation to run as admin. This is also an issue in the WiX extension for IIS as described in this bug report. See my previous article of a workaround for both the UI Sequence part and the bug: Using a bootstrapper to force elevated privileges in Vista

If you did not quite understand what I just said about the sequence stuff, elevated privileges etc., relax and keep reading, you will know by the end of this post and/or by clicking the link above :-)

Overview

This article assume you’ve read my previous article about Using WiX to author MSI installations or that you are somewhat familiar with WiX. If you haven’t done so already and want to follow this article step-by-step, download the WiX project from my previous article here: SimpleWebApp.zip

There is another possibility of course :-) If you’re lazy and just want the complete source for this article, you can download everything from here: SimpleWebApp_WSSelect.zip

Since this article became rather long I’ve structured it into 3 main sections with related sub sections, which hopefully will help you later if you need to look up something:

Now let’s dig into it!

Custom actions

To add a CA to your solution, do this:

  1. Add a new class library project to your solution and name it IISCustomAction
  2. Add a reference to Microsoft.Deployment.WindowsInstaller found in the WiX SDK
  3. Add a reference to System.DirectoryServices
  4. Rename the class created by VS to CustomAction.cs
  5. Add the following using’s to CustomAction.cs:
    1. using System.DirectoryServices;
    2. using Microsoft.Deployment.WindowsInstaller;

When building your project a couple of things happens:

  • The managed dll (IISCustomAction.dll) is created as expected by a class library
  • MakeSfxCA.exe (which is automatically called when building in VS) is creating a new dll based on the managed dll named IISCustomAction.CA.dll (this is the one you need to use in your WiX project)

It’s MakeSfxCA.exe that does all the magic here. The output dll that MakeSfxCA has created is actually a Win32 DLL. Here’s what Christopher Painter says about this:

At runtime, MSI thinks it’s calling a Win32 DLL in it’s own sandbox but in reality the CLR is being fired up out of process and communicated with through a named pipe.

Read the complete article here.

A typical CA looks something like this:

[CustomAction]
public static ActionResult MyCustomAction(Session session)
{
    try
    {
        ...
    }
    catch (Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

The [CustomAction] attribute is needed to tag this method as a CA. The ActionResult returns either (in my case) Failure or Success allowing the installer to respond accordingly. The parameter for this method is a Session object. This object gives me access to the MSI database and the inner workings of Windows Installer allowing me to query the database, access MSI properties etc.

Now you have what you need to start writing CA’s for the Windows Installer, so let’s get cranking.

Custom action – Get web sites
Copy and paste the code below into the CustomAction.cs class in the project you created above:

[CustomAction]
public static ActionResult GetWebSites(Session session)
{
    try
    {
        View listBoxView = session.Database.OpenView("select * from ListBox");
        View availableWSView = session.Database.OpenView("select * from AvailableWebSites");
        DirectoryEntry iisRoot = new DirectoryEntry("IIS://localhost/W3SVC");
        int order = 1;
        foreach (DirectoryEntry webSite in iisRoot.Children)
        {
            if (webSite.SchemaClassName.ToLower() == "iiswebserver" && 
                webSite.Name.ToLower() != "administration web site")
            {
                StoreWebSiteDataInListBoxTable(webSite, order, listBoxView);
                StoreWebSiteDataInAvailableWebSitesTable(webSite, availableWSView);
                order++;
            }
        }
    }
    catch (Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

Here’s what I’ve done in the code above:

  • Open two views to the msi database. One for the ListBox table and one for a custom table which I will cover later called AvailableWebSites.
  • Get the root entry in IIS by using an LDAP query.
  • Iterate every child of the web site to get and store the information I need.

Here’s the code for the two helper methods I use to store the IIS data to the Windows Installer database:

private static void StoreWebSiteDataInListBoxTable(DirectoryEntry webSite, int order, View listBoxView)
{
    Record newListBoxRecord = new Record(4);
    newListBoxRecord[1] = "WEBSITE";
    newListBoxRecord[2] = order;
    newListBoxRecord[3] = webSite.Name;
    newListBoxRecord[4] = webSite.Properties["ServerComment"].Value;
    listBoxView.Modify(ViewModifyMode.InsertTemporary, newListBoxRecord);
}
private static void StoreWebSiteDataInAvailableWebSitesTable(DirectoryEntry webSite, View availableWSView)
{
    //Get Ip, Port and Header from server bindings
    string[] serverBindings = ((string)webSite.Properties["ServerBindings"].Value).Split(':');
    string ip = serverBindings[0];
    string port = serverBindings[1];
    string header = serverBindings[2];
    Record newFoundWebSiteRecord = new Record(5);
    newFoundWebSiteRecord[1] = webSite.Name;
    newFoundWebSiteRecord[2] = webSite.Properties["ServerComment"].Value;
    newFoundWebSiteRecord[3] = port;
    newFoundWebSiteRecord[4] = ip;
    newFoundWebSiteRecord[5] = header;
    availableWSView.Modify(ViewModifyMode.InsertTemporary, newFoundWebSiteRecord);
}

The first method stores data in the ListBox table and the second in my custom AvailableWebSites table.

My custom table (which I’ll explain in more detail later) contains the following columns:

  • WebSiteNo
  • WebSiteDescription
  • WebSitePort
  • WebSiteIP
  • WebSiteHeader

The ListBox table has these four columns:

  • Property
  • Order
  • Value
  • Text

For the ListBox table the Property is used for identifying one specific list box and will allow you to get the Value of the selected item later. The Property must be the same for every record that you want displayed in one single UI list box. Order defines in which order the item are displayed in the list. Value is what’s being returned to you when accessing the property later on. The Text field is what is shown in the UI list box that the end user sees in the MSI wizard.

In my code I set the Property for all records to WEBSITE. The Order is set to 1 the first time and then incremented by 1 every iteration. The Value is set to the web site id and theText is set to the web site name.

After the user have selected the web site we can find out which one by using the WEBSITE property, which will give us the id of the web site (the Value field).

In my custom AvailableWebSites table I store some more details about every web site. But before I can do that I need to get the server bindings from IIS which is formatted like this: ip:port:header. I split these up and add them to to the database. I do the same with WebSiteNo and WebSiteDescription.

In both cases I store the values to the record and update the database (using modify). Note that you have to use InsertTemporary because you’re not allowed to write permanent data to the MSI database during installation.

Custom action – Add selected web site info to properties
After the user have selected a web site in the list box, I call a CA where I store the details about the web site to some public properties. This will make more sense later when we stitch everything together in the WiX product file, but for now here’s the code:

[CustomAction]
public static ActionResult UpdatePropsWithSelectedWebSite(Session session)
{
    try
    {
        string selectedWebSiteId = session["WEBSITE"];
        session.Log("CA: Found web site id: " + selectedWebSiteId);
        View availableWebSitesView = session.Database.OpenView("Select * from AvailableWebSites where WebSiteNo=" + selectedWebSiteId);
        availableWebSitesView.Execute();
        Record record = availableWebSitesView.Fetch();
        if ((record[1].ToString()) == selectedWebSiteId)
        {
            session["WEBSITE_DESCRIPTION"] = (string)record[2];
            session["WEBSITE_PORT"] = (string)record[3];
            session["WEBSITE_IP"] = (string)record[4];
            session["WEBSITE_HEADER"] = (string)record[5];
        }
    }
    catch(Exception ex)
    {
        session.Log("CustomActionException: " + ex.ToString());
        return ActionResult.Failure;
    }
    return ActionResult.Success;
}

Remember the id for the web site that the user selected is stored in the WEBSITE property in the ListBox table? I can get this value by calling session[“WEBSITE”]. I then query my custom table that I populated earlier for details about this web site using Select with the id of the web site in the where clause. I then store these values to the properties: WEBSITE_DESCRIPTION, WEBSITE_PORT, WEBSITE_IP and WEBSITE_HEADER. These properties and where they came from will be explained later :-)

Create a new dialog

To have the user see and select from available web sites, we need to create a dialog displaying a list box. Add a new wxs file to your project and name it SelectWebSiteDlg.wxs. Then replace any generated xml with this xml:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
  <Fragment>
    <CustomAction Id="UpdatePropsWithSelectedWebSite" BinaryKey="WebSiteCA" DllEntry="UpdatePropsWithSelectedWebSite" Execute="immediate" Return="check" />
    <Binary Id="WebSiteCA" SourceFile="$(var.SolutionDir)\IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />
  </Fragment>
  <Fragment>
    <UI>
      <Dialog Id="SelectWebSiteDlg" Width="370" Height="270" Title="Select Web Site">
        <Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
        <Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
        <Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
          <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
        </Control>
        <Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="Please select which web site you want to install to." />
        <Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="Select Web Site" />
        <Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
        <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
        <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
        <Control Id="SelectWebSiteLabel" Type="Text" X="20" Y="60" Width="290" Height="14" NoPrefix="yes" Text="Select web site:" />
        <Control Id="SelectWebSiteCombo" Type="ListBox" X="20" Y="75" Width="200" Height="150" Property="WEBSITE" Sorted="yes" />
      </Dialog>
    </UI>
  </Fragment>
</Wix>

This xml file is separated into two fragments; one for the CA and one for the actual UI. The CA entry define in which assembly the CA is located and defines an id we can use later when calling the CA. This CA is the one I created previously for updating some properties with IIS data. The reason I’m defining it here is that I can, and it makes sense to group it with the dialog that are using it. Because, as you will see later, we’re going to call this CA after the user have clicked next on this dialog.

The UI section is where the layout of the actual UI components is defined. Most elements are default for any dialog, except SelectWebSiteLabel and SelectWebSiteCombo which is the two controls specific to the web site dialog.

I also need to update MyUI.wxs which controls which and in which order the dialogs are displayed in the wizard. I’ve added the SelectWebSiteDlg between InstallDirDlg and VerifyReadyDlg as shown here:

<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="SelectWebSiteDlg" Order="6"><![CDATA[WIXUI_INSTALLDIR_VALID="1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Next" Event="DoAction" Value="UpdatePropsWithSelectedWebSite" Order="1">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="2">1</Publish>
<Publish Dialog="SelectWebSiteDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">1</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="SelectWebSiteDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>

See that I use DoAction to call the CA I defined earlier? This is how you hook up a CA to the “click event” of a button (or any control for that matter). Not quite as easy as in Win Forms, but it’s still quite logical.

Also note that I’ve changed the Next event for InstallDirDlg and the Back event for VerifyReadyDlg to point to my new dialog. This so the wizard will navigate correctly when the user clicks the next and back buttons.

Changes to Product.wxs (the main WiX file)

Custom Table – Storing web site info
I promised to cover my custom table in more detail later, so here it is. First the code:

<CustomTable Id="AvailableWebSites" >
  <Column Id="WebSiteNo" Category="Identifier" PrimaryKey="yes" Type="int" Width="4" />
  <Column Id="WebSiteDescription" Category="Text" Type="string" PrimaryKey="no"/>
  <Column Id="WebSitePort" Category="Text" Type="string" PrimaryKey="no"/>
  <Column Id="WebSiteIP" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
  <Column Id="WebSiteHeader" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
  <Row>
    <Data Column="WebSiteNo">0</Data>
    <Data Column="WebSiteDescription">Bogus</Data>
    <Data Column="WebSitePort">0</Data>
    <Data Column="WebSiteIP"></Data>
    <Data Column="WebSiteHeader"></Data>
  </Row>
</CustomTable>

When a user have selected the web site to install to, instead of querying IIS for details about the web site (web site number, description, port etc), I can just get it from my AvailableWebSite table. Note that I’ve added some dummy data in the first row. Without this the custom table was not stored to the MSI and I did not find any other way of getting this to work.

Properties: Define, store and retrieve
Earlier I created a CA that stored info about the selected web site into some public properties. Where did these properties come from? Well, they’re defined in the Product.wxs file like this:

<Property Id="WEBSITE_DESCRIPTION">
  <RegistrySearch Id="WebSiteDescription"
          Name="WebSiteDescription"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_PORT">
  <RegistrySearch Id="WebSitePort"
          Name="WebSitePort"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_IP">
  <RegistrySearch Id="WebSiteIP"
          Name="WebSiteIP"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>
<Property Id="WEBSITE_HEADER">
  <RegistrySearch Id="WebSiteHeader"
          Name="WebSiteHeader"
          Root="HKLM"
          Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
          Type="raw" />
</Property>

I’ve also defined a registry search within each property. Why? In order to have access to these values during e.g. repair or uninstall we need to store them in the registry for later use. Why? Well, the Windows Installer does not maintain state (except from INSTALLDIR and maybe a few others). Meaning any values or selections a user did during installation is not kept for later use. Since we will need to know which web site the user installed the application to, so we can remove it on uninstall, we need to store the state to the registry. If the application has been installed before, the registry search above get’s these values from the registry and stores them in their respective properties.

But how did they end up in the registry to begin with? I do that by defining a new component like this:

<Component Id="PersistWebSiteValues" Guid="C3DAE2E2-FB49-48ba-ACB0-B2B5B726AE65">
  <RegistryKey Root="HKLM" Key="SOFTWARE\torresdal.net\SimpleWebApp\Install">
    <RegistryValue Name="WebSiteDescription" Type="string" Value="[WEBSITE_DESCRIPTION]"/>
    <RegistryValue Name="WebSitePort" Type="string" Value="[WEBSITE_PORT]"/>
    <RegistryValue Name="WebSiteIP" Type="string" Value="[WEBSITE_IP]"/>
    <RegistryValue Name="WebSiteHeader" Type="string" Value="[WEBSITE_HEADER]"/>
  </RegistryKey>
</Component>

And we need to reference the component under the Feature tag:

<ComponentRef Id="PersistWebSiteValues" />

Registry key’s in WiX require a component, which is good. That means that these keys/values will be added on install and removed on uninstall, saving us manual work. The registry key above is created at SOFTWARE\torresdal.net\SimpleWebApp\Install. The actual values are the properties as you see in the Value attributes. To reference a property just use [MY_PROPERTY]. This comes in handy many times when authoring MSI installations.

Reference CA’s
One of the CA’s (the UpdatePropsWithSelectedWebSite) is actually already referenced in the SelectWebSiteDlg, but we need to reference the GetWebSites CA as well:

<CustomAction Id="GetIISWebSites" BinaryKey="IISCA" DllEntry="GetWebSites" Execute="immediate"  Return="check" />
<Binary Id="IISCA" SourceFile="$(var.SolutionDir)IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />

The CustomAction tag defines a logical entry that we can use later to call this CA by using the id GetIISWebSites. In addition there is a Binary node that tells where WiX can find this CA when build the project. The CustomAction node point to this by using the BynaryKey attribute.

Then we need to make sure it’s being called:

<InstallUISequence>
  <Custom Action="GetIISWebSites" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
</InstallUISequence>

The above code tells the installer to run this CA in the UISequence only when the application is not already installed (defined by the NOT Installed condition).

Change the WebSite standard action
In order for the installer to pick up the properties we’ve set for the selected web site (description, port, ip etc) we need to change the code we had previously:

<iis:WebSite Id='DefaultWebSite' Description='Default Web Site'>
    <iis:WebAddress Id='AllUnassigned' Port='80' />
</iis:WebSite>

To this:

<iis:WebSite Id="SelectedWebSite" Description="[WEBSITE_DESCRIPTION]">
  <iis:WebAddress Id="AllUnassigned" Port="[WEBSITE_PORT]" IP="[WEBSITE_IP]" Header="[WEBSITE_HEADER]" />
</iis:WebSite>

Makes sense right? I’ve also changed the id to SelectedWebSite which is more accurate in this case, meaning you need to update the WebSite reference in WebVirtualDir in the IISAppplication component as well. If you fail to do this the WiX compiler will complain, so you’re in safe hands :-)

The complete Product.wxs
There’s a lot of snippets above, so for your convenience I’ve included the complete code for Product.wxs here so you see everything together:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
  <Product Id="77da05c4-1644-4bc3-ac14-c0f721fe31fe" Name="Simple Web App" Language="1033" Version="1.0.0.0" Manufacturer="Jon Torresdal" UpgradeCode="a897ccc5-1c81-47e0-a837-65c07a72a7bc">
    <Package InstallerVersion="400" Compressed="yes" />
    <Media Id="1" Cabinet="WixProject1.cab" EmbedCab="yes" />
    <Property Id="TARGETVDIR" Value="SimpleWebApp"/>
    <Property Id="WEBSITE_DESCRIPTION">
      <RegistrySearch Id="WebSiteDescription"
              Name="WebSiteDescription"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_PORT">
      <RegistrySearch Id="WebSitePort"
              Name="WebSitePort"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_IP">
      <RegistrySearch Id="WebSiteIP"
              Name="WebSiteIP"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <Property Id="WEBSITE_HEADER">
      <RegistrySearch Id="WebSiteHeader"
              Name="WebSiteHeader"
              Root="HKLM"
              Key="SOFTWARE\torresdal.net\SimpleWebApp\Install"
              Type="raw" />
    </Property>
    <CustomAction Id="GetIISWebSites" BinaryKey="IISCA" DllEntry="GetWebSites" Execute="immediate"  Return="check" />
    <Binary Id="IISCA" SourceFile="$(var.SolutionDir)IISCustomAction\bin\Debug\IISCustomAction.CA.dll" />
    <InstallUISequence>
      <Custom Action="GetIISWebSites" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
    </InstallUISequence>
    <Directory Id="TARGETDIR" Name="SourceDir">
      <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="SimpleWebApp">
          <Component Id="Default.aspx" Guid="fc46b1a2-35e9-4a17-896b-80b2daaae567">
            <File Id="Default.aspx" Name="Default.aspx" Source="$(var.SolutionDir)SimpleWebApp\Default.aspx" DiskId="1" KeyPath="yes" />
          </Component>
          <Component Id="Web.config" Guid="2ED81B77-F153-4003-9006-4770D789D4B6">
            <File Id="Web.config" Name="Web.config" Source="$(var.SolutionDir)SimpleWebApp\Web.config" DiskId="1" KeyPath="yes" />
            <util:XmlFile Id="AppSettingsAddNode" File="[INSTALLLOCATION]Web.config" Action="createElement" ElementPath="/configuration/appSettings" Name="add" Sequence="1" />
            <util:XmlFile Id="AppSettingsKeyAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="key" Value="AddedDuringInstall" Sequence="2" />
            <util:XmlFile Id="AppSettingsValueAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="value" Value="This text was added during installation." Sequence="3" />
          </Component>
          <Directory Id="binFolder" Name="bin">
            <Component Id="SimpleWebApp.dll" Guid="7FC6DA37-12E5-463d-8E7E-08F73E40CCF2">
              <File Id="SimpleWebApp.dll" Name="SimpleWebApp.dll" Source="$(var.SolutionDir)SimpleWebApp\Bin\SimpleWebApp.dll" DiskId="1" KeyPath="yes" />
            </Component>
          </Directory>
        </Directory>
      </Directory>
      <Directory Id="ProgramMenuFolder">
        <Directory Id="MyWebAppStartMenuFolder" Name="SimpleWebApp">
          <Component Id="StartMenuFolder" Guid="B3AEC4C4-3F8E-4865-B87A-B750533776B5" >
            <util:InternetShortcut Id="SimpleWebAppShortcut" Name="SimpleWebApp" Target="http://localhost/SimpleWebApp/Default.aspx" Directory="MyWebAppStartMenuFolder" />
            <RemoveFolder Id="RemoveStartMenuFolder1" On="uninstall"/>
            <RegistryKey Root="HKCU" Key="SOFTWARE\torresdal.net\SimpleWebApp\SimpleWebAppShortcut">
              <RegistryValue Type="string" Value="Default Value"/>
            </RegistryKey>
          </Component>
        </Directory>
      </Directory>
      <Component Id="IISApplication" Guid="FFA12D9C-5AEC-45f8-AA7D-5C4CEC7FA466">
        <iis:WebAppPool Id="SWAAppPool" Name="SWAAppPool" />
        <iis:WebVirtualDir Id="VirtualDir" Alias="[TARGETVDIR]" Directory="INSTALLLOCATION" WebSite="SelectedWebSite">
          <iis:WebApplication Id="SimpleWebAppApp" Name="[TARGETVDIR]" WebAppPool="SWAAppPool" />
          <iis:WebDirProperties Id="WebVirtualDirProperties" Execute="yes" Script="yes" Read="yes" WindowsAuthentication="no" AnonymousAccess="yes" IIsControlledPassword="yes" />
        </iis:WebVirtualDir>
      </Component>
      <Component Id="PersistWebSiteValues" Guid="C3DAE2E2-FB49-48ba-ACB0-B2B5B726AE65">
        <RegistryKey Root="HKLM" Key="SOFTWARE\torresdal.net\SimpleWebApp\Install">
          <RegistryValue Name="WebSiteDescription" Type="string" Value="[WEBSITE_DESCRIPTION]"/>
          <RegistryValue Name="WebSitePort" Type="string" Value="[WEBSITE_PORT]"/>
          <RegistryValue Name="WebSiteIP" Type="string" Value="[WEBSITE_IP]"/>
          <RegistryValue Name="WebSiteHeader" Type="string" Value="[WEBSITE_HEADER]"/>
        </RegistryKey>
      </Component>
    </Directory>
    <CustomTable Id="AvailableWebSites" >
      <Column Id="WebSiteNo" Category="Identifier" PrimaryKey="yes" Type="int" Width="4" />
      <Column Id="WebSiteDescription" Category="Text" Type="string" PrimaryKey="no"/>
      <Column Id="WebSitePort" Category="Text" Type="string" PrimaryKey="no"/>
      <Column Id="WebSiteIP" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
      <Column Id="WebSiteHeader" Category="Text" Type="string" PrimaryKey="no" Nullable="yes"/>
      <Row>
        <Data Column="WebSiteNo">0</Data>
        <Data Column="WebSiteDescription">Bogus</Data>
        <Data Column="WebSitePort">0</Data>
        <Data Column="WebSiteIP"></Data>
        <Data Column="WebSiteHeader"></Data>
      </Row>
    </CustomTable>
    <iis:WebSite Id="SelectedWebSite" Description="[WEBSITE_DESCRIPTION]">
      <iis:WebAddress Id="AllUnassigned" Port="[WEBSITE_PORT]" IP="[WEBSITE_IP]" Header="[WEBSITE_HEADER]" />
    </iis:WebSite>
    <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
    <UIRef Id="MyUI" />
    <Feature Id="ProductFeature" Title="SimpleWebApp" Level="1">
      <ComponentRef Id="Default.aspx" />
      <ComponentRef Id="Web.config" />
      <ComponentRef Id="SimpleWebApp.dll" />
      <ComponentRef Id="StartMenuFolder" />
      <ComponentRef Id="IISApplication" />
      <ComponentRef Id="PersistWebSiteValues" />
    </Feature>
  </Product>
</Wix>

The End! 
That’s that! If you’ve come this far you (hopefully) have a functional CA with goodies that allow your user to select web sites in your installer. Hope you found this post useful and let me know if you find any errors or questions, and I’ll help you out. Btw here’s the end result:

MsiSelectWebSite

.Net | CSharp | Deployment | IIS | WiX
Friday, October 24, 2008 10:41:23 PM (W. Europe Daylight Time, UTC+02:00)
Saturday, October 11, 2008

WixVS Earlier this week I posted a question on the WiX user list. The question was: Which version of WiX is being distributed with Visual Studio 2010?

As you probably know by know (at least if your interested in msi and deployment) is that WiX will ship as part of Visual Studio 2010. To make a long story short, the answer is 3.0. This was expected, but I was still curious. Having WiX v. 3.0 with VS means that all developers have the option to create advanced installations using Windows Installer, and also to create Custom Actions (CA’s) in managed code!

Saturday, October 11, 2008 12:33:05 AM (W. Europe Daylight Time, UTC+02:00)
Monday, October 06, 2008

This is my third post in my WiX and DTF series for WiX 3.0. Here are some others I’ve written:

Normally bootstrappers are used for adding prerequisites to your installation; like the .Net Framework, Windows Installer etc. You can also use it for other purposes though. Basically it's an exe file with the msi (and any other installers) embedded. For the WiX series I'm currently doing we need this because:

  1. Use as workaround for the WiX bug in IIS extension where the installer fails because of missing privileges on Vista SP1.
  2. We want to do IIS custom actions in InstallUISequence which require elevated privileges which I will cover in a later post (hopefully very soon!).

I briefly mentioned InstallUISequence above. Windows Installer have several sequences, for details you can check them out here, but I'm just going to shortly explain two of them now. The InstallUISequence is the wizard part in the installer, InstallExecuteSequence is where things are happening and files are getting installed (when you click "Install" in the wizard).

Here's a bit of background. In Vista msi files does not run elevated (as admin) when you start them directly (double click on the msi). To do that you need to open an elevated command prompt and run the msi with msiexec /i yourInstaller.msi. That's not a good solution.

Normally there should be no need for admin privileges in UISequence (it's a reason it's not permitted per default), but there are exceptions, like the above. At least from my perspective. If you have a better way of solving this, I'm very interested.

So, with a bootstrapper or exe file you can do a bit more. For instance you can use mt.exe to add a manifest to he exe defining which privileges it requires to run. In this article I will describe how you can accomplish this using WiX and .Net.

Here's how to go about it:

  1. Copy Setup.exe found in the WiX bin folder to your project location (so we don't modify the original)
  2. Create a manifest file (e.g. Vista.manifest) in your favorite text editor by adding this xml:
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    
    <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
    
      <assemblyIdentity version="1.0.0.0" processorArchitecture="X86" name="Setup" type="win32" />
    
      <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    
        <security>
    
          <requestedPrivileges>
    
            <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
    
          </requestedPrivileges>
    
        </security>
    
      </trustInfo>
    
    </assembly>
  3. Add the manifest to Setup.exe by using mt.exe found in the .Net SDK:

    mt.exe -manifest [pathToYourProject\]Vista.manifest -outputresource:[pathToYourProject\]Setup.exe;#1

  4. Use setupbld.exe found in the WiX bin folder to embed the msi installer into a exe like this:

    setupbld.exe" -out Setup.exe -msu MyMsi.msi -setup Setup.exe

    or even better, add it as a post-build event (Right click your project -> Properties -> Build Event -> Post-build Event Command Line):

    "C:\Program Files\Windows Installer XML v3\bin\setupbld.exe" -out $(TargetDir)Setup.exe -msu "$(TargetPath)" -setup "$(ProjectDir)setup.exe"

The interesting thing to note about the manifest file is this:

<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />

When setting the level to requireAdministrator Vista will show a UAC prompt when executing this file. The default level is asInvoker, which will run the exe with the current users privileges, but then the exe would have the same behavior as the msi.

Also note the assemblyIdentity node where you can set the actual version number of your installer and the name (if you use something else than Setup.exe).

.Net | Deployment | Security | Vista | WiX
Monday, October 06, 2008 12:47:02 PM (W. Europe Daylight Time, UTC+02:00)
Friday, September 26, 2008

This is my second post in my WiX and DTF series for WiX 3.0. Here are some others I’ve written:

The source for this WiX demo can be downloaded here: SimpleWebApp.zip

PS! This demo is only tested on Windows Vista. If you have any issues on other versions of Windows, please let me know.

Update: There is a bug in the WiX extension for IIS on Vista SP 1 resulting in a install failure (Failed to read IIsWebSite table. (-2147024891)). Currently the only workaround is to run the msi from an elevated command prompt using msiexec /i nameOfInstaller.msi.
I'll post an update as soon as I find a better solution or the WiX team publish a bug fix.

Update2: You can work around the error mentioned above by using the approuch I've described here: Using a Bootstrapper To Force Elevated Privileges In Vista

The short time I've used WiX has been a joy. I feel so much more in control over the functionality and possibilities of Windows Installer than with any other tool. I have experience with both InstallShield and Wise, but there I found myself looking around in all corners of the UI and the MSI database to find the functionality I was looking for. However, without the knowledge from these two products the transition to WiX would not have been that easy.

In WiX everything is at your fingertips, even though it's XML :-) It is however good if you have a bit of knowledge about Windows Installer and how it works. At first I was skeptical to WiX using XML, but with Visual Studio intellisense and a nice integration for e.g. build functionality, it's way better than any other xml authoring tool I've tried. Saying that, the documentation for WiX is a bit incomplete. This is especially true for WiX 3.0. However, there are other sources of documentation to WiX than their web site. There's quite a few blog posts out there, the WiX mailing list and a couple of Wiki's.

Download and install

Now down to the essence of WiX; authoring MSI installations. Before you start you need to download WiX and install. In my article I will assume you have Visual Studio installed, but you can certainly do the same things without Visual Studio, though with a bit of extra work.

After you have installed WiX you should have a few new templates in Visual Studio. I'm using VS 2008, where it looks like this:

WixProjects

Creating a new WiX project

  1. File -> New -> Project
  2. Select Project type WiX, Template WiX Project and click OK

That was that! Not too hard was it? So what do we have now?

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Product Id="77da05c4-1644-4bc3-ac14-c0f721fe31fe" Name="WixProject1" Language="1033" Version="1.0.0.0" Manufacturer="WixProject1" UpgradeCode="a897ccc5-1c81-47e0-a837-65c07a72a7bc">
        <Package InstallerVersion="200" Compressed="yes" />

        <Media Id="1" Cabinet="WixProject1.cab" EmbedCab="yes" />

        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFilesFolder">
                <Directory Id="INSTALLLOCATION" Name="WixProject1">
                    <!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
                    <!-- <Component Id="ProductComponent" Guid="fc46b1a2-35e9-4a17-896b-80b2daaae567"> -->
                        <!-- TODO: Insert files, registry keys, and other resources here. -->
                    <!-- </Component> -->
                </Directory>
            </Directory>
        </Directory>

        <Feature Id="ProductFeature" Title="WixProject1" Level="1">
            <!-- TODO: Remove the comments around this ComponentRef element and the Component above in order to add resources to this installer. -->
            <!-- <ComponentRef Id="ProductComponent" /> -->
        </Feature>
    </Product>
</Wix>

We have an installation definition that can generate an msi that does nothing! :-) Let's modify it to do something useful...

Adding installer UI

By default WiX uses the simplest installer UI possible, meaning none but the default shown by Windows Installer. This means if you want your users to interact with your installation, you need to customize it a bit. WiX comes with the following built-in dialog sets that will make this easy:

  • WixUI_Mondo
  • WixUI_FeatureTree
  • WixUI_InstallDir
  • WixUI_Minimal
  • WixUI_Advanced

While creating the installer I will try to follow best practices for Windows Installer, have it work with Vista and Server 2008, and make it pass the Certified for Windows Vista test cases. That means we need to allow the user to select which folder she wants the application installed to. Which one of the above can help us out with that? You guessed it; WixUI_InstallDir.

  1. Add a reference to WixUIExtension.dll (right click project -> Add referece...)
  2. Add these two lines between the Directory and Feature tags in your Product.wxs file:
        <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
        <UIRef Id="WixUI_InstallDir" />

The property WIXUI_INSTALLDIR must be set for the UI to know which directory to use as default. Here I set it to INSTALLLOCATION which is the Id of the directory where the application is being installed. The UIRef tag allow us to reference an external package of UI's. In this case the WixUI_InstallDir in the WixUIExtension library.

Changing the installer UI

In this section we're going to remove the license agreement dialog from the UI and change the graphics displayed in the Windows Installer wizard.

WixUI_InstallDir actually includes several dialogs. Among others the license agreement dialog and the install dir dialog. For my installer I don't want the license agreement, so how do I remove it?

The easiest way is to download the WiX source and get the wxs file for WixUI InstallDir and make it your own (before you think this is too cumbersome, try it. It's actually quite fast and simple, and you learn a thing or two by looking at the source files) :

  1. Right click your WiX project -> Add -> New Item...
  2. Select WiX File and give it a name (e.g. MyUI.wxs)
  3. In a text editor, open the file WixUI_InstallDir.wxs found (in the version I'm using) here: ...Wix-3.0.4318.0-sources\src\ext\UIExtension\wixlib
  4. Copy the xml
  5. Paste it into your new WiX file (MyUI.wxs)
  6. Comment out the license agreement dialogs (see source below)
  7. Change the navigation targets for Next and Back buttons for WelcomeDlg and InstallDirDlg to not use LicenseAgreementDlg anymore (see source below)

The next thing I want to do is change the welcome screen and the top banner of the installation with my own graphics. Here's how:

  1. Add a new folder to your vs project and name it Images
  2. Create two images (if you don't want to create your own, you can download mine by clicking on links for the images)
    1. msibanner.jpg (493x58)
    2. msiwelcome.jpg (493x312)
  3. Copy your images to the Images folder
  4. Next add the two lines below to your UI file (MyUI.wxs) just below the Fragment node to have the installation use the new images:
        <WixVariable Id="WixUIBannerBmp" Value="Images\msibanner.jpg" />
        <WixVariable Id="WixUIDialogBmp" Value="Images\msiwelcome.jpg" />

Here's the MyUI.wxs file after the changes:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <WixVariable Id="WixUIBannerBmp" Value="Images\msibanner.jpg" />
        <WixVariable Id="WixUIDialogBmp" Value="Images\msiwelcome.jpg" />
        
        <UI Id="MyUI">
            <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
            <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
            <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />

            <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
            <Property Id="WixUI_Mode" Value="InstallDir" />

            <DialogRef Id="BrowseDlg" />
            <DialogRef Id="DiskCostDlg" />
            <DialogRef Id="ErrorDlg" />
            <DialogRef Id="FatalError" />
            <DialogRef Id="FilesInUse" />
            <DialogRef Id="MsiRMFilesInUse" />
            <DialogRef Id="PrepareDlg" />
            <DialogRef Id="ProgressDlg" />
            <DialogRef Id="ResumeDlg" />
            <DialogRef Id="UserExit" />

            <Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath" Order="3">1</Publish>
            <Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>

            <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>

            <!-- Changed WelcomeDlg next button to navigate to InstallDirDlg-->
            <!--<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>-->
            <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">1</Publish>

            <!-- Removed LicenseAgreementDlg -->
            <!--
            <Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
            <Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">LicenseAccepted = "1"</Publish>
            -->

            <!-- Changed InstallDirDlg back button to navigate to WelcomeDlg -->
            <!--<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg">1</Publish>-->
            <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
            
            <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
            <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">1</Publish>
            <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
            <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID="1"]]></Publish>
            <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
            <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>

            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
            <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed</Publish>

            <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>

            <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
            <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
            <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>

            <Property Id="ARPNOMODIFY" Value="1" />
        </UI>

        <UIRef Id="WixUI_Common" />
    </Fragment>
</Wix>

If you now build your project you can see how the new installer looks like by running the MSI file which you'll find in the debug folder of your project.

Adding files, folders, shortcuts and changing Web.config

A nice UI for the installer is all good, but without installing anything useful it's not much point is it? So let's add some files.

Adding files
Since I'm later going to demo how to select which web site to install to (by using a custom action), it's natural that we're going to install a web application. I've created a very simple web app that has three files: Default.aspx, Web.config and SimpleWebApp.dll. So let's add them to the installation by changing Product.wxs:

Add directories, components, files and feature ref's as shown below:

<Directory Id="TARGETDIR" Name="SourceDir">
    <Directory Id="ProgramFilesFolder">
        <Directory Id="INSTALLLOCATION" Name="SimpleWebApp">
            <Component Id="Default.aspx" Guid="fc46b1a2-35e9-4a17-896b-80b2daaae567">
                <File Id="Default.aspx" Name="Default.aspx" Source="$(var.SolutionDir)SimpleWebApp\Default.aspx" DiskId="1" KeyPath="yes" />
            </Component>
            <Component Id="Web.config" Guid="2ED81B77-F153-4003-9006-4770D789D4B6">
                <File Id="Web.config" Name="Web.config" Source="$(var.SolutionDir)SimpleWebApp\Web.config" DiskId="1" KeyPath="yes" />
            </Component>
            <Directory Id="binFolder" Name="bin">
                <Component Id="SimpleWebApp.dll" Guid="7FC6DA37-12E5-463d-8E7E-08F73E40CCF2">
                    <File Id="SimpleWebApp.dll" Name="SimpleWebApp.dll" Source="$(var.SolutionDir)SimpleWebApp\Bin\SimpleWebApp.dll" DiskId="1" KeyPath="yes" />
                </Component>
            </Directory>
        </Directory>
    </Directory>
</Directory>
...
<Feature Id="ProductFeature" Title="SimpleWebApp" Level="1">
    <ComponentRef Id="Default.aspx" />
    <ComponentRef Id="Web.config" />
    <ComponentRef Id="SimpleWebApp.dll" />
</Feature>

One thing to note in is the use of $(var.SolutionDir) in the Source attribute. This is a reference to a Visual Studio variable. In this case the path for the solution directory.

Adding a shortcut
Now let's add a shortcut to the start menu. To do this we need a reference to another WiX component: WixUtilExtension. To get intellisense in VS add the following xml namespace to the Wix node:

xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"

And then add this xml:

<Directory Id="TARGETDIR" Name="SourceDir">
...
<
Directory Id="ProgramMenuFolder"> <Directory Id="MyWebAppStartMenuFolder" Name="SimpleWebApp"> <Component Id="StartMenuFolder" Guid="B3AEC4C4-3F8E-4865-B87A-B750533776B5" > <util:InternetShortcut Id="SimpleWebAppShortcut" Name="SimpleWebApp" Target="http://localhost/SimpleWebApp/Default.aspx" Directory="MyWebAppStartMenuFolder" /> <RemoveFolder Id="RemoveStartMenuFolder1" On="uninstall"/> <RegistryKey Root="HKCU" Key="SOFTWARE\torresdal.net\SimpleWebApp\SimpleWebAppShortcut"> <RegistryValue Type="string" Value="Default Value"/> </RegistryKey> </Component> </Directory> </Directory> </Directory>

The directory ProgramMenuFolder is a standard directory id for the program menu folder. Under that folder we add another folder for our application; the SimpleWebApp. To add the actual shortcut we need a component. In this component we define the shortcut by using util:InternetShorcut. In addition to this we must make sure to remove the SimpleWebApp folder un uninstall done by using the RemoveFolder action. And last we need to associate the component with a key. Usually components automatically links to a file for its key, but since there is no file here we must provide a registry key instead. This is done using the RegistryKey and RegistryValue actions.

And since we've created a new component we need to add that to the feature:

<ComponentRef Id="StartMenuFolder" />

Changing Web.config
Now let's do some changes to Web.config during installation. Under the component for Web.config add this xml that will add an element to appSettings in the config file:

<util:XmlFile Id="AppSettingsAddNode" File="[INSTALLLOCATION]Web.config" Action="createElement" ElementPath="/configuration/appSettings" Name="add" Sequence="1" />
<util:XmlFile Id="AppSettingsKeyAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="key" Value="AddedDuringInstall" Sequence="2" />
<util:XmlFile Id="AppSettingsValueAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="value" Value="This text was added during installation." Sequence="3" />

The first line will add the 'add' node below the appSettings node. The second line will add the 'key' attribute and the third line will add the 'value' attribute. WiX also provide a util called XmlConfig which is suppose to be customized for .Net config files. However, I was unable to get it to work as expected. Please let me know if you find out how to use XmlConfig instead.

Adding the web application to IIS

To work with IIS, WiX has another extension we can use; the IISExtension. Add the reference and define the xml namespace:

xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension"

Then add the following xml under the root directory TARGETDIR:

<Component Id="IISApplication" Guid="FFA12D9C-5AEC-45f8-AA7D-5C4CEC7FA466">
    <iis:WebAppPool Id="SWAAppPool" Name="SWAAppPool" />
    <iis:WebVirtualDir Id="VirtualDir" Alias="[TARGETVDIR]" Directory="INSTALLLOCATION" WebSite="DefaultWebSite">
        <iis:WebApplication Id="SimpleWebAppApp" Name="[TARGETVDIR]" WebAppPool="SWAAppPool" />
        <iis:WebDirProperties Id="WebVirtualDirProperties" Execute="yes" Script="yes" Read="yes" WindowsAuthentication="no" AnonymousAccess="yes" IIsControlledPassword="yes" />
    </iis:WebVirtualDir>
</Component>

And also since this is a new component, we need to add it to the feature:

<ComponentRef Id="IISApplication" />

We also need to define the default web site which the virtual directory is pointing to:

<iis:WebSite Id='DefaultWebSite' Description='Default Web Site'>
    <iis:WebAddress Id='AllUnassigned' Port='80' />
</iis:WebSite>

The iis:WebAppPool will add a new application pool for this web application. This is strictly not necessary, but I like my web apps to run in its own app pool so I can recycle only that web app without affecting other apps. iis:WebVirtualDir adds a virtual directory to IIS under Default Web Site. In addition to this (since we have an asp.net app) we want to define a web application by using iis:WebApplication. This is set up to use the application pool defined earlier. And the last item is the properties for the virtual directory defined by iis:WebDirProperties. For further details about these items, have a look at the schema reference found in the WiX documentation.

Giving the installer a proper name

To have the installer to use Simple Web App as the product name and not WixProject1, change the Name attribute of the Product node like this:

<Product Id="77da05c4-1644-4bc3-ac14-c0f721fe31fe" Name="Simple Web App" Language="1033" Version="1.0.0.0" Manufacturer="Jon Torresdal" UpgradeCode="a897ccc5-1c81-47e0-a837-65c07a72a7bc">

And also if you want a different name of the msi you can changed that on the properties page of the WiX project. Just set the output name to something else.

The complete source

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension" xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">
    <Product Id="77da05c4-1644-4bc3-ac14-c0f721fe31fe" Name="Simple Web App" Language="1033" Version="1.0.0.0" Manufacturer="Jon Torresdal" UpgradeCode="a897ccc5-1c81-47e0-a837-65c07a72a7bc">
        <Package InstallerVersion="200" Compressed="yes" />

        <Media Id="1" Cabinet="WixProject1.cab" EmbedCab="yes" />
        <Property Id="TARGETVDIR" Value="SimpleWebApp"/>
        
        <Directory Id="TARGETDIR" Name="SourceDir">
            <Directory Id="ProgramFilesFolder">
                <Directory Id="INSTALLLOCATION" Name="SimpleWebApp">
                    <Component Id="Default.aspx" Guid="fc46b1a2-35e9-4a17-896b-80b2daaae567">
                        <File Id="Default.aspx" Name="Default.aspx" Source="$(var.SolutionDir)SimpleWebApp\Default.aspx" DiskId="1" KeyPath="yes" />
                    </Component>
                    <Component Id="Web.config" Guid="2ED81B77-F153-4003-9006-4770D789D4B6">
                        <File Id="Web.config" Name="Web.config" Source="$(var.SolutionDir)SimpleWebApp\Web.config" DiskId="1" KeyPath="yes" />
                        <util:XmlFile Id="AppSettingsAddNode" File="[INSTALLLOCATION]Web.config" Action="createElement" ElementPath="/configuration/appSettings" Name="add" Sequence="1" />
                        <util:XmlFile Id="AppSettingsKeyAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="key" Value="AddedDuringInstall" Sequence="2" />
                        <util:XmlFile Id="AppSettingsValueAttribute" Action="setValue" File="[INSTALLLOCATION]Web.config" ElementPath="/configuration/appSettings/add" Name="value" Value="This text was added during installation." Sequence="3" />
                    </Component>
                    <Directory Id="binFolder" Name="bin">
                        <Component Id="SimpleWebApp.dll" Guid="7FC6DA37-12E5-463d-8E7E-08F73E40CCF2">
                            <File Id="SimpleWebApp.dll" Name="SimpleWebApp.dll" Source="$(var.SolutionDir)SimpleWebApp\Bin\SimpleWebApp.dll" DiskId="1" KeyPath="yes" />
                        </Component>
                    </Directory>
                </Directory>
            </Directory>
            <Directory Id="ProgramMenuFolder">
                <Directory Id="MyWebAppStartMenuFolder" Name="SimpleWebApp">
                    <Component Id="StartMenuFolder" Guid="B3AEC4C4-3F8E-4865-B87A-B750533776B5" >
                        <util:InternetShortcut Id="SimpleWebAppShortcut" Name="SimpleWebApp" Target="http://localhost/SimpleWebApp/Default.aspx" Directory="MyWebAppStartMenuFolder" />
                        <RemoveFolder Id="RemoveStartMenuFolder1" On="uninstall"/>
                        <RegistryKey Root="HKCU" Key="SOFTWARE\torresdal.net\SimpleWebApp\SimpleWebAppShortcut">
                            <RegistryValue Type="string" Value="Default Value"/>
                        </RegistryKey>
                    </Component>
                </Directory>
            </Directory>
            <Component Id="IISApplication" Guid="FFA12D9C-5AEC-45f8-AA7D-5C4CEC7FA466">
                <iis:WebAppPool Id="SWAAppPool" Name="SWAAppPool" />
                <iis:WebVirtualDir Id="VirtualDir" Alias="[TARGETVDIR]" Directory="INSTALLLOCATION" WebSite="DefaultWebSite">
                    <iis:WebApplication Id="SimpleWebAppApp" Name="[TARGETVDIR]" WebAppPool="SWAAppPool" />
                    <iis:WebDirProperties Id="WebVirtualDirProperties" Execute="yes" Script="yes" Read="yes" WindowsAuthentication="no" AnonymousAccess="yes" IIsControlledPassword="yes" />
                </iis:WebVirtualDir>
            </Component>
        </Directory>

        <iis:WebSite Id='DefaultWebSite' Description='Default Web Site'>
            <iis:WebAddress Id='AllUnassigned' Port='80' />
        </iis:WebSite>

        <Property Id="WIXUI_INSTALLDIR" Value="INSTALLLOCATION" />
        <UIRef Id="MyUI" />

        <Feature Id="ProductFeature" Title="SimpleWebApp" Level="1">
            <ComponentRef Id="Default.aspx" />
            <ComponentRef Id="Web.config" />
            <ComponentRef Id="SimpleWebApp.dll" />
            <ComponentRef Id="StartMenuFolder" />
            <ComponentRef Id="IISApplication" />
        </Feature>
    </Product>
</Wix>

Build and install

That's it! Now lets build the project and install the application.

Deployment | Web | WiX
Friday, September 26, 2008 2:37:43 AM (W. Europe Daylight Time, UTC+02:00)
Thursday, September 04, 2008

This is my first post in my WiX and DTF series for WiX 3.0. Here are some others I’ve written:

Windows Installer XML (WiX)

The version of WiX I'm going to talk about is v.3.0, currently in beta. In essence WiX is an XML format for writing MSI deployment packages. Much like using InstallShield or Wise to write your installations, you can use WiX to do the same. So why do you want to use XML to author your msi's? At first I thought: "Ohhh no! Not another XML based authoring tool!" But after doing some more digging, this is one of the scenarios were XML makes sense... kind of. Here's the benefits I see with WiX compared to traditional MSI tools:

  • It's perfect for "code" generation, using Nant, TFS etc.
  • WiX is very clean, so it gives you the possibility to author exactly what you need and not have all the general stuff that IS and Wise has pre added to your installation

Another thing to note is that WiX is open source and it comes from Microsoft (yea, Microsoft HAVE open source projects). Not only that, but this tool got so popular within Microsoft that most of their packages are now moving towards using WiX, and many of them, like Office, already use WiX. That says a lot!

I'll get back to using WiX to author msi installations in a later post, but now I want to focus on DTF and how to use it without WiX.

Deployment Tools Foundation (DTF)

DTF is among other things a managed (.net) wrapper on top of msi.dll (the windows installer library). DTF makes it possible to write custom actions (CA's) for MSI installations in C#, VB.net or any other .Net language. If you're not used to working with MSI installations this might not be a big deal for you, but for me this is fantastic news! Not only that, debugging has now become much easier (or should i say possible).

Before you only had three options for writing CA's: C++, VB Script and Java Script. CA's are custom code you write because there are currently no existing action supported by MS Installer that does what you're looking for.

Examples of things you can do with Windows Installer out of the box are:

  • Install/delete/move files
  • Create/delete/change folders
  • Create/delete/change shortcuts
  • Install/remove/start/stop Windows Services
  • Reboots
  • Install/remove ODBC drivers
  • Register/un-register COM/COM+ applications
  • Register/un-register fonts
  • Write/change registry
  • ...and so on...

Check this out for a more complete list.

However, Windows Installer can't cover all scenarios, so then you turn to CA's. Here are some examples I've used CA's for:

  • List SQL Servers
  • List IIS Web Sites
  • Create Web Sites and Virtual Directories
  • Change XML files

You should however know this; writing good CA's is difficult and there is a lot of things to take into account. For instance, what happens if your installation fails and starts rolling back? Then you need to make sure that any changes you've done to the system with your CA also needs to roll back, meaning you have to have two CA's, one for adding stuff and one for removing stuff.

Why not just stick to VB/Java script?

Using VB and Java scripts has become more and more difficult because of security restrictions, virus programs etc. Many virus programs will not allow you to use Windows Scripting Host (WSH) at all.

About 2 months ago I did some work on our existing msi packages related to a Microsoft certification program. The requirements for this certification forced me to adopt the best practices that I should have done a long time ago. To avoid the problem with scripting I had two options. Rewrite my CA's in C++, or in C# using DTF. After digging around it was an easy choice. I went for managed code.

However, this might not be the right solution for you. Managed code require the .Net framework, so if you don't know if .Net is present on the computer running your software, you might want to stick to "traditional" CA's. For me this was not a problem, since all computers (clients and servers) we install on already have .Net pre-installed.

Next

Next time I'll show you how to create a CA for listing available IIS Web Sites on IIS7. This will allow you to choose which web site to install your application under. Until then I suggest you check out WiX/DTF and start thinking if this is something for you.

.Net | Deployment | Tools | WiX
Thursday, September 04, 2008 12:29:31 AM (W. Europe Daylight Time, UTC+02:00)
Tuesday, June 03, 2008

Sysinternals Don't want to download and install all the tools from Sysinternals? Want to have access from anywhere on any computer? Just hook up to the UNC path: \\live.sysinternals.com\tools and you can run them directly. Thanks to John St. Clair for letting me know. Is that cool or what?

For more information about the different tools go to http://technet.microsoft.com/sysinternals.

Tuesday, June 03, 2008 8:36:07 PM (W. Europe Daylight Time, UTC+02:00)
Tuesday, May 13, 2008

Update: This article is not limited to deploying applications to Vista, but also when changing files that are only writeable by administrators (e.g. files in the Program Files directory).

If you have not made specific changes to your installation to support UAC in Vista, this is for you. I hope this will help others to not spend many days of confusion, desperation and sleepless nights like I have.

Lately I've been getting reports from some of my coworkers running Windows Vista that the app.config file in our application was not updated after installation. I've also experienced this myself. We've solved them without knowing exactly how (which I hate). Typically "I uninstalled the application and installed to a different location, and it worked!" Well, this is a workaround that are not particulary popular to any of our customers, so I dived into the problem to find the exact reason.

To save you some reading I'll just say that the reason and solution is to provide your application with a manifest file to handle UAC in Vista as described in this article: Create and Embed an Application Manifest (UAC) I haven't actually tried this out yet, but I trust the article to be correct :-)

Virtualization
But that's not the interesting part. The interesting part is what happens (or what can happen) if you don't have an application manifest for this purpose? If your superstitious you might think that something weird is going on with your computer. If you're like me you KNOW that something weird is going on with your computer, and you need to find it and fix it. The weird thing is Vista virtualization.

"File virtualization addresses the situation where an application relies on the ability to store a file, such as a configuration file, in a system location typically writeable only by administrators"

"Virtualization is implemented to improve application compatibility problems for applications running as a standard user on Windows Vista."

As I write this I have exactly this problem on my computer. I've installed our application (Contiki ECM) to this location: C:\Program Files\CMA Contiki AS\Contiki ECM\. This is where I've always install our app. So today I installed and checked that the app.config was updated with all my changes done during installation. And it hadn't. Actually it was updated, but not with my changes. It looked like an old file that I had some time ago. How could this happen?

Textpad .vs. Notepad
To make this even more confusing, I discovered this reading the file in TextPad, but when opening the file in Notepad it was fine. All my changes was there! So I thought that this was a TextPad problem. I started our application, but it turns out that when .Net tries to read the config file, it reads the same old stuff as TextPad did, resulting in old config to be loaded into our app. Why does Notepad get the right file? I don't know. Please tell me if you do.

The solution
So after Googling for a while I found this article on MSDN, and things started to make sense. But first let me show you what it looks like on my computer. The file causing my headaches are Contiki.Windows.Application.exe.config.

VistaUNCImg1 
Do you see when it's modified? 08:56 this morning, but the file that is being opened is from 13th of April! How do I know? Well, if you have a look at the virtualized files which you'll find in C:\Users\Username\AppData\Local\VirtualStore\[Your path], it might help out. So I looked at C:\Users\1jontor\AppData\Local\VirtualStore\Program Files\CMA Contiki AS\Contiki ECM and found this:

VistaUNCImg2 
Modified back in April. I opened this file and it was exactly what I saw in TextPad. But this file is the same both in TextPad and Notepad! :-)

Explorer gives you a shortcut to the Virtual Store folder if any files are virtualized, and will show this button in Explorer:

VistaUNCImg3

The end
A couple of notes from the MSDN article:

"When you enumerate resources in folders and in the registry, Windows Vista will merge global file/folder and registry keys into a single list. In this merged view, the global (protected) resource is listed along with the virtualized resource."

"The virtual copy will always be present to the application first.... even if [some file] is updated"

Even if the file is updated!? Who though of that? Was that really such a good idea? I can see what they where thinking, but it would have saved me a lot of work getting an error message saying "Access denied"...

To end this off I picked another good quote from the article:

"Microsoft intends to remove virtualization from future versions of the Windows operating system as more applications are migrated to Windows Vista."

I appreciate that... :-)

Tuesday, May 13, 2008 10:08:42 PM (W. Europe Daylight Time, UTC+02:00)
Wednesday, May 07, 2008

Today I was struggling to figure out why I kept getting this error message when downloading a ClickOnce app:

The file 'C:\Documents and Settings\[user]\Local Settings\Temp\2\Deployment\RCAX3K4W.T8E\GRY27V4Q.GBE\SomeDll.dll' already exists.

First, let me just say that I don't use Visual Studio to create the ClickOnce deployment, but the Mage.exe utility. If you use Visual Studio this problem will be solved on Publish in Visual Studio.

I knew there was only one of this dll present on the server where ClickOnce downloads its files, but still it insisted that it was already downloaded. I checked my application manifest and there where only one entry for my dll. The dll causing this problem is a COM dll with generated manifest files (Reg-Free COM) as described here.

I started to inspect the Reg-Free COM manifest file generated by Visual Studio. It turns out that this manifest file use the same syntax as ClickOnce, or actually ClickOnce use the existing manifest schema that was introduced with Windows XP and Reg-Free COM. So in my ClickOnce manifest I had this:

...
<file name="My.dll" size="1121280">
  <hash>
    <dsig:Transforms>
      <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
    </dsig:Transforms>
    <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
    <dsig:DigestValue>UwpS1EpkD7aJk3uWdOtX+3BHiOA=</dsig:DigestValue>
  </hash>
</file>
...

...and in my Reg-Free COM manifest I had this:

...
<file name="My.dll" asmv2:size="1121280">
    <hash xmlns="urn:schemas-microsoft-com:asm.v2">
      <dsig:Transforms>
        <dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
      </dsig:Transforms>
      <dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
      <dsig:DigestValue>UwpS1EpkD7aJk3uWdOtX+3BHiOA=</dsig:DigestValue>
    </hash>
    <typelib tlbid="{2d5e2d34-bed5-4b9f-9793-a31e26e6806e}" version="4.1" helpdir="" resourceid="0" flags="HASDISKIMAGE" />
    ...COM Registry Entries...
</file>
...

It's exactly the same as ClickOnce except from the <typelib> part which is used by Reg-Free COM. ClickOnce treated this as any other ClickOnce manifest, and correctly stated that the file is about to be downloaded twice. The surprising part for me though was that the Reg-Free COM manifest was read at all. Since the deployment manifest is only linked to the application manifest...

So what's the solution? Just delete the file entry in the ClickOnce manifest and re-sign it! Deleting can be done like this:

ApplicationManifest manifest = (ApplicationManifest) ManifestReader.ReadManifest("ApplicationManifest", @"My.exe.manifest", false);
FileReference fileRef = manifest.FileReferences.FindTargetPath("My.dll");
manifest.FileReferences.Remove(fileRef);
ManifestWriter.WriteManifest(manifest);

Curious about how Visual Studio solves this? It merges all the manifest files into the application manifest and use the imported manifest entries instead (or something like that). At least that happened when I created a sample program to test this.

Wednesday, May 07, 2008 12:05:41 AM (W. Europe Daylight Time, UTC+02:00)
Thursday, March 13, 2008

MouseClickServer deployment of ClickOnce? Isn't ClickOnce client deployment? Yes, it is. However, you will have to deploy the ClickOnce files to a server, right? I feel this is a poorly documented step and I thought I share some of my experience around this.

So what's the problem? The main issue is the deployment manifest file. If you've worked with ClickOnce you know about this file, if not here's a (very) short description. The deployment manifest includes information about where the ClickOnce application is located (e.g. http://myServer/myWD/myApp.application) as showed in the following snippet:



...
<
deployment install="true"
mapFileExtensions="true"
trustURLParameters="true"> <subscription> <update> <beforeApplicationStartup /> </update> </subscription> <deploymentProvider
codebase="http://myServer/myWD/myApp.application"
/> </deployment>
...

This file needs to be signed with a certificate to be valid, which means that any changes to this file makes it invalid and you'll have to resign it. This is a good thing for security, but it causes some problems regarding deployment. The url specified in this file must be changed when deployed at a server. Unless you want to get all information needed from your customer and create the deployment package before sending it to them or force them to set up a server named myServer ;-). I don't think that solution is very likely to be used in production systems. So what you need to do, is make the installation (e.g. msi) change the manifest file and resign it during installation.

Microsoft has provided us with a tool to do exactly this. However, you're not allowed to redistribute it. Another issue is that you need to have a certificate for signing and you don't want to have your corporate certificate laying around in your msi. The most common solution around this problem is to prompt the user during installation for a certificate (like suggested here). But then your app is signed by someone else's certificate, which is probably not what you want.

I will now suggest a different solution to this problem. In the System.Build.Tasks.dll you'll find much of the functionality needed to do the same thing as Microsoft's Mage.exe is doing. The nice thing about this dll is that it's a part of the default .Net Framework installation. Here's a short description of the classes needed:

DeployManifest
An object model for the manifest file. Gives you direct access to specific parts of the manifest and makes it really easy to make changes.

ManifestReader
Have a static method (ReadManifest) which let you read and return the manifest you are going to work with.

ManifestWriter
Use the static method WriteManifest to write your changes back to the manifest file.

X509Certificate2
Your certificate to sign the manifest file with.

SecurityUtiities
Has the static method called SignFile which let you sign your manifest.

By using the classes described above it should be quite easy to achieve the same as with the Microsoft utility. Here's an example:

using System;
using System.IO;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Build.Tasks.Deployment.ManifestUtilities;
...
string
manifestPath = @"C:\Temp\myApp.application"; string deploymentUrl = "http://myServer/myWD/myApp.application"; DeployManifest manifest =
(DeployManifest)ManifestReader.
ReadManifest("DeployManifest", manifestPath, false); manifest.DeploymentUrl = deploymentUrl; ManifestWriter.WriteManifest(manifest); X509Certificate2 certificate =
new
X509Certificate2(GetCertificateFile(), "password"); SecurityUtilities.SignFile(certificate, null, manifestPath);

There is one important step we need to solve. How do we protect the certificate we need to sign the manifest file. For this I suggest you incorporate this into your tool (either a command line tool or a .Net library) by using Build Action = Embedded Resource. This will make your certificate inaccessible for most people. If you want to secure it even more, you can crypt it in however way you want. I leave this up to you ;-) In my code above I've done this in the GetcertificateFile() method:

X509Certificate2 certificate = 
new
X509Certificate2(GetCertificateFile(), "password");

There is of course one other issue which you'll have to consider. What if someone uses your custom tool to sign their ClickOnce app? So you need to protect this as well with a password or some other mechanism. If you have a good suggestion to this, please let me know.

Thursday, March 13, 2008 3:03:15 PM (W. Europe Standard Time, UTC+01:00)
Friday, February 08, 2008

Update: I've found a temporary solution. After inspecting which files was in our package I found a 3rd party component that we don't use anymore but still had a reference to. This was references by our business layer, which all our web services use, hence this dll was in every bin folder on every web service. Since the size of the file was quite big it was enough to get the installation down to a reasonable size and avoiding the described error message.

For a while now our msi file that contains the application server for our product has not been working. Not an msi that our customers have, but one we're about to ship in two weeks. I've had my hands full for a long time, but today I finally got around to check what was causing the problem. Here's a screen shot of the error I get:

MsiInstallError

After a quick Google search I found that this is related to a bug on Windows Server 2003 and Windows XP. Cause:

This problem occurs if the Windows Installer process has insufficient contiguous virtual memory to verify that the .msi package or the .msp package is correctly signed. 

Fantastic! I will not go and ask all our customers to install the hotfix on their servers. That's just to stupid. About now you're probably thinking something like "How big is their app-server really?" or "What product can get to big for an msi file?". I can tell you... The size of the msi is 576MB. No images, no database, just pure .Net dll's! And actually we're not alone. Visual Studio Service Pack 1 had the exact same problem! Go figure. And even better, there is no hotfix for this on Windows XP.

Have we been coding like mad men for a decade and producing tons of code? I wish (or maybe not) :) The answer is simpler than you might think. Our application server contains web services only. At the beginning of our project we took an architectural decision to have our web services separated in virtual directories (or projects in VS). This to be able to update parts of our application without affecting the whole system. This was part of a master plan around the smart client principle and would let us deploy small modules into our application which dynamically loads the new behavior. As it turns out we have almost never done this in production. The drawback of this is of course that you get many of the same dll's scattered around in many virtual directories. And when you have many web services (by web services I mean asmx files and not web methods, 43 to be exact), and each one is dependent on your business and data access layer (which is quite huge) and more, megabytes is piling up.

So what's the solution? Well, since we don't really use this as intended we can just merge our web services into one and we will be home free. Except you don't do that two weeks before deployment with acceptance testing coming up. So I have to think of something else. The only solution I can see now is to find some way of making the msi smaller, which at the current time I don't have the faintest idea of how to accomplish. I can use a setup launcher (exe) which will probably solve it, but that will not make our customers very happy. Hopefully I'll figure something out during the weekend, or maybe you have a solution?

Friday, February 08, 2008 10:37:08 PM (W. Europe Standard Time, UTC+01:00)
RSS RSS - Comments Twitter LinkedIn
         
SEARCH
 
 
         
TOP POSTS
   
         
NAVIGATION
   
         
CATEGORIES
  .Net (61) ADFS (3) Agile (30) Ajax (5) Architecture (20) Articles (1) ASP.NET (6) ASP.NET-MVC (1) Blogging (12) Books (2) BPEL (1) CleanCode (1) CloudComputing (7) Community (4) CSharp (11) DasBlog (5) Database (2) DDD (5) Deployment (16) DSL (1) Events (38) ExtremeProgramming (6) Fun (6) Gadgets (4) IIS (10) InfoQ (4) Java (2) Lean (3) Linq (2) MemoryLeaks (5) Microsoft (37) MVC (1) NDC (2) NNUG (36) Other (10) Patterns (9) Performance (3) Scrum (17) Security (7) ServiceBus (1) Silverlight (4) Software (19) TeamManagement (11) TechEd (7) Testing (4) Tools (25) TvGuide (1) WCF (8) Web (15) WebDeploy (1) WIF (3) Windows (10) Vista (15) VisualStudio (16) WiX (9) Work (16) Workflow (3)  
         
ARCHIVE
   
         
BLOGROLL
   
         
ON THIS PAGE...
 
No-Click Web Deployment – Part 2 – Web Deploy (a.k.a. msdeploy)
No-Click Web Deployment – Part 1
WiX 3.0 Released
Just got podcasted
WiX 3.0 in Beta… Officially that is…
WiX and DTF: Debug a Managed Custom Action and how to generate an MSI log
WiX and DTF: Using a Custom Action to list available web sites on IIS
Which version of WiX is going to be distributed with VS 2010
WiX and DTF: Using a bootstrapper to force elevated privileges in Vista
WiX and DTF: Using WiX to author MSI installations
WiX and DTF: Introduction
Sysinternals tools on file share
Are you deploying applications to Windows Vista, and doing ok?
Problem with Reg-Free COM and ClickOnce
ClickOnce - Server Deployment
Our MSI installation got too big for Win Server 2003!