.NET Setup Deployment - MSI, Cassini, SQL Server, NTFS


By Robbe Morris
Printer Friendly Version
  

Creating any moderately complex MSI based installation with the Visual Studio .NET 2005 setup project is a real pain. Today's tips will include how to easily package up a single installation file, setup SQL Server 2005, execute large sql scripts, launch the application at the end of setup, configure NTFS permissions, trigger another MSI file, and auto install and configure UltiDev's Cassini Web Server.



As I'm sure you've read elsewhere, you can hook into the installer's events and trigger your own C#/VB.NET code.  The easiest way to do this is to create a separate class library project in your solution and add the class below:

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;
using System.Configuration.Install;
using System.Collections;
using System.IO;
using System.Diagnostics;

namespace YourNamespace
{
    [System.ComponentModel.RunInstallerAttribute(true)]
    public class MyInstall : System.Configuration.Install.Installer
    {
        const string ASSEMBLYPATH_STATENAME = "assemblypath";
        private Container components=null;
 
        public MyInstall()
        {
         // This call is required by the Designer.
         InitializeComponent();
         this.Committed += new InstallEventHandler(MyInstall_Committed);
        }
 
        private void InitializeComponent()
        {

        }
 
       private void WriteTextFile(string fileName,
                                  string contents)
       {
         try
         {

           DeleteFile(fileName);

           using (StreamWriter sw = new StreamWriter(fileName))
           {
             sw.Write(contents);
           }
         }
         catch (Exception) { throw; }
       }
 
       private void DeleteFile(string fileName)
       {
         try
         {

          if (File.Exists(fileName))
          {
            File.Delete(fileName);
          }

         }
         catch (Exception) { throw; }
        }
 
       public override void Install(IDictionary stateSaver)
       {
         base.Install(stateSaver);
       }
 
       public override void Rollback(IDictionary savedState)
       {
         base.Rollback(savedState);
       }

       public override void Commit(IDictionary savedState)
       {
        base.Commit(savedState);
       }
 
       public override void Uninstall(IDictionary savedState)
       {
         base.Uninstall(savedState);
       }

       /// <summary> 
       /// Clean up any resources being used.
       /// </summary>
       protected override void Dispose(bool disposing)
       {
         if (disposing)
         {
           if (components != null)
           {
              components.Dispose();
           }
         }
         base.Dispose(disposing);
       }
 
      private void MyInstall_Committed(object sender,
                                       InstallEventArgs e)
      {
        string path = "";
        string appName = "";
        string args = "";
        System.Diagnostics.Process process = null;

        try
        {
      
          path = GetParameter("assemblypath");
          path = path.Replace(@"\TheClassLibraryThisInstallerClassIsIn.dll","");
          appName =Path.Combine(path,"YourWindowsFormsApplicationName.exe");

          args = "\"" + path + "\"";

          // Why am I passing the applications install
          // directory as an argument?  For some reason,
          // an application launched from an MSI will "think"
          // its execution path is C:\windows\system32 if
          // from "inside" the application you try to determine
          // its app path.  Very odd...
 
          // autostart is a radio button parameter
          // from the installer dialog window you
          // created that asked if the user wanted
          // to auto start or not.  And, you added

// the following line in the CustomActionData
// property of your custom action:
// /autostart=[AUTOSTART]


// If your command line argument can have
// spaces, your CustomActionData would look like this:
// /autostart="[AUTOSTART]"
if (GetParameter("autostart") == "1") { process = new System.Diagnostics.Process(); process.Start(appName, args); process.StartInfo.FileName = appName; process.StartInfo.Arguments = args; process.Start(); // If you ever want to launch another // app or even an msi, you can use // process.WaitForExit(); To wait for

// each one to finish.
} } catch (Exception ex) { WriteTextFile(Path.Combine(path,"install.log"), ex.Message); } } private string GetParameter(string parameterKey) { if (Context.Parameters[parameterKey] == null) { return String.Empty; } return Context.Parameters[parameterKey].Trim(); } } }

Then, add that project/assembly to the list of primary output items
for the installer.  In the installer's Custom Item section, add
a custom item for each of the events and target it to your newly
added installation "hook" project. You'll find a reference to it
in the Application Folder when creating the Custom Item.

That's it. You can now run anything you want straight from the installer.

Now the real fun begins. You want to package up the dotnetfx.exe,
sqlexpr32.exe, the .msi and anything else you need to one file. Why?

If you choose to have .net and sql server installed as a later
download, you run the risk of install interruptions. Plus,
the .NET install routes the user to a web site where they must
choose the right version. Do you really want users to have to guess?

Ideally, we want the user to download one file and have the setup
take care of the rest. So, the only real reliable way to do this is to
spend $50 and buy Winzip's zip self extraction tool. You set your
installer to use the local copy of the prerequisites and build your
installation.

Upon completion, zip up all the files into myapplication.zip.
Then, run winzip's self extraction creator and set it to run as a software installation. When the user downloads your .exe, everything
will happen easily and automatically for your user.

After installing SQL Server 2005 Express, you are going to get a nasty
little surprise that you should have expected. The windows account
"Network Service" that the SQL Server services runs under most likely doesn't
have write access to the folder you will eventually run CREATE DATABASE on.

So, you'll need to set NTFS permissions on that folder first. Here's
a quick .NET 2.0 sample:


using System;
using System.Collections.Generic;
using System.Text;
using System.Security.AccessControl; 
using System.IO;

public static bool GrantModifyAccessToFolder(string windowsAccountUserName,
                                             string folderName)
{
  DirectoryInfo directory = null;
  DirectorySecurity directorySecurity = null;
  FileSystemAccessRule rule = null;

  try
  {

    if (windowsAccountUserName.Length <1) { return false; }
    if (folderName.Length <1) { return false; }
    if (!Directory.Exists(folderName)) { return false; }

    directory = new DirectoryInfo(folderName);

    directorySecurity = directory.GetAccessControl();

    rule = new FileSystemAccessRule(windowsAccountUserName,
                                    FileSystemRights.Modify,
                                    InheritanceFlags.None |
                                    InheritanceFlags.ContainerInherit |
                                    InheritanceFlags.ObjectInherit,
                                    PropagationFlags.None,
                                    AccessControlType.Allow);

    directorySecurity.SetAccessRule(rule);

    directory.SetAccessControl(directorySecurity);

    return true;

  }
  catch (Exception) { throw; }
}



Cassini Web Server Installation

You'll want to include UltiDev.com's CassiniExplorerSetup.msi
and CassiniServer2Setup.msi as files to deploy in your application
setup .msi file. Whenever you are ready, you pass in the root
folder of where your application will be installed and it will
create a subfolder called "website" and set it to be the root
folder of the web application.
private void InstallWebServer(string appPath)
{

  string cassiniLocation = "";
  string cassiniExplorer = "";
  string args = "";
  System.Diagnostics.Process msi = null;

  try
  {

   cassiniLocation = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles), @"UltiDev\Cassini Web Server for ASP.NET 2.0\UltiDevCassinWebServer2.exe"); cassiniExplorer = Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFiles), @"UltiDev\Cassini Web Server Explorer\LocalStart.htm"); if (System.IO.File.Exists(cassiniLocation)) { // Cassini is already installed return; } msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = "msiexec"; msi.StartInfo.Arguments ="/passive /i \"" + Path.Combine(appPath, "CassiniExplorerSetup.msi") + "\""; msi.Start(); msi.WaitForExit(); msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = "msiexec"; msi.StartInfo.Arguments = "/passive /i \"" + Path.Combine(appPath, "CassiniServer2Setup.msi") + "\""; msi.Start(); msi.WaitForExit(); } catch (Exception) { // decide what you want to have happen here? // IIS maybe? throw; } try { // UltiDev's cassini wants a GUID as the website // identifier and the 90210 is a hard coded port // (use any port you want) when registering // your website. args = "/register \"" + Path.Combine(appPath, "website"); args += "\" someGUIDgoeshere Default.aspx 90210 /DontKeepRunning"; msi = new System.Diagnostics.Process(); msi.StartInfo.FileName = cassiniLocation; msi.StartInfo.Arguments = args; msi.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; msi.Start(); msi.WaitForExit(); MessageBox.Show("Cassini web server installed."); } catch (Exception ex) { if (System.IO.File.Exists(cassiniExplorer)) { Process.Start(cassiniExplorer); } } }
// Sometimes, you'll need to execute large SQL Server scripts 
// with GO statements.  The following method will execute
// them without the need to parse the string:

using Microsoft.SqlServer.Management.Smo;

using Microsoft.SqlServer.Management.Common;

public void ExecSql(string sql, string connectionString,string dataBaseNameToPrepend) { try { sql = sql.Trim(); if (sql.Length <1 ) { return; } if (dataBaseNameToPrepend != null) { if (dataBaseNameToPrepend.Trim().Length > 0) { sql = "USE ["+ dataBaseNameToPrepend.Trim() + "]\nGO\n" + sql; } } using (SqlConnection conn = new SqlConnection(connectionString)) {
conn.Open(); Server server = new Server(new ServerConnection(conn)); server.ConnectionContext.ExecuteNonQuery(sql); server.ConnectionContext.Disconnect(); } } catch (Exception ex) {
// You'll need to pass back the inner exception
// to get anything useful for errors thrown using
// Microsoft.SqlServer.Management.Smo throw new Exception(ex.InnerException.Message); } }

There you have it. You should be able to drastically improve your installation capabilities without having to resort to some expensive installation software that is even more of a pain to learn and use.


Biography
Robbe is a 2004-2008 Microsoft MVP for C# and the .NET Evangelist for Alinean Inc..  He is also the co-founder of EggHeadCafe. Robbe enjoys scuba diving with the folks at wet-n-fla.


button
 
Article Discussion: .NET Setup Deployment - MSI, Cassini, SQL Server, NTFS
  Robbe Morris posted at 20-Oct-07 11:42
Original Article

 
  Can't make this work
  Alexis Coles replied to Robbe Morris at 20-May-08 04:26

Hi Robbe,

Thanks for posting this, this is the only resource that I have found for launching another MSI from set up and deploy.  I have found the standard tools very restrictive and its good to know that you can add some customallity through scripts.

However I'm quite new to programing and having some problems getting this to work.

All I want to do is launch a second MSI that is allready made at the end of the one that I am creating.  I have copied your first class from the example, and am trying to get it to build, I have changed the following code...

path = GetParameter("assemblypath");

path = path.Replace(@"\class1.dll", "");

appName = Path.Combine(path, "PBCLTRT110.msi");

class1 is the class libary that I added to the solution, and PBCLTRT110.msi is the MSI I am trying to launch.

I am gettin the following errors;

Error 1 Static member 'System.Diagnostics.Process.Start(string, string)' cannot be accessed with an instance reference; qualify it with a type name instead C:\Documents and Settings\alexis.coles\My Documents\Visual Studio 2005\Projects\Alfi Setup\RunMSI\Class1.cs 135 21 RunMSI

RunMSI being the namespace I have to class1 and

Error 2 Unspecified module entry point for custom action 'C:\Documents and Settings\alexis.coles\My Documents\Visual Studio 2005\Projects\Alfi Setup\RunMSI\obj\Debug\RunMSI.dll'. C:\Documents and Settings\alexis.coles\My Documents\Visual Studio 2005\Projects\Alfi Setup\Alfi Setup\Alfi Setup.vdproj Alfi Setup

I guess the second one is something to do with the /autostart=[AUTOSTART] Line of code that I have left commented out as I do not really understand what to do with it.

Would be very greatfull if you could explaine these parts in a little more detail.

Many thanks


 
  Post the code for your whole class
  Robbe Morris replied to Alexis Coles at 20-May-08 08:30
Looking at the error message, I can only summize that you have changed a lot of the code.  Also, are you "sure" your class1.dll is named just "class1.dll" or is the real file "RunMSI.Class1.dll".  Double check your code while debugging to ensure that the appName variable has the right path to your second msi.