.NET Setup Deployment - MSI, Cassini, SQL Server, NTFS
By Robbe Morris
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.
Popularity (7966 Views)
 |
| Biography - Robbe Morris |
| Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of EggHeadCafe.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice. Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. |
 |
|
|
Article Discussion: .NET Setup Deployment - MSI, Cassini, SQL Server, NTFS
Can't make this work
Alexis Coles replied
to Robbe Morris at Saturday, October 20, 2007 12:10 PM
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 Saturday, October 20, 2007 12:10 PM
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.
Doesn't work with an .msi
Sergey Kulikov replied
to Robbe Morris at Saturday, October 20, 2007 12:10 PM
Hi Robbe,
Thank you for the great post. I've got it working with any application, but I can't run an .msi. I always get error saying " Another installation is already in progress. Complete that installation before proceeding with this install."
Any help is greatly appreciated.
Thank you.
Asp.Net WebSetUp
prabhu rsp replied
to Sergey Kulikov at Saturday, October 20, 2007 12:10 PM
Hi,
Thanks its very good article.In my case While executing asp.net setup file ( By single click ) i have to install some more softwares such as sql server 2005, xp service pack 2 , configuration files etc.
Also before installing this softwares if already exists these softwares on that machine/server should display in gride format those softwares already installed remainning softwares not installed ( ex : while installing .Net we can know what are the components already installed and which components not installed ) using check box or anything.
How to do this if any links please give me some idea.
by
Prabhu.R
SQL SERVER 2005 installation in silent mode
Palanivel Rajan replied
to Robbe Morris at Saturday, October 20, 2007 12:10 PM
Hi,
I would like to know by what language SQL SERVER 2005 setup is written.
I am about to develop an application to automate the silent mode installation of SQL SERVER 2005 in a machine which has no .NET framework.
I am in a way choosing which language to do develop that application.
Regards,
Palani
You don't need to know that at all
Robbe Morris replied
to Palanivel Rajan at Saturday, October 20, 2007 12:10 PM
SQL SERVER 2005 installation in silent mode
Palanivel Rajan replied
to Robbe Morris at Saturday, October 20, 2007 12:10 PM
Hi,
Thanks :)
I dont want to store information like userid, password in a static setup file i.e. .ini file.
Instead I want to write an application which displays 1 screen/window which will get these informations from user at the time of setup and generates the setup file. Then my application will make use of the generated set up file and kick start the SQL SERVER 2005 setup.
So i am in a way of chosing a programming language to write this application.
They cannot be .NET languages as the machine wont be having .NET framework prior to the installation of SQL SERVER 2005.
Will VB, VC++, Windows Installer can help me to achieve this?
Thanks & Regards,
Palani
The .net framework will get auto installed by the msi
Robbe Morris replied
to Palanivel Rajan at Saturday, October 20, 2007 12:10 PM
and then fire your installer. And, you can indeed launch .net windows apps to take over on the install process.
VB requires the runtime engine to pre-exist which isn't always there. Same thing for C++.
I've written these things with techniques from that article for the exact situation you describe. It is very easy to implement.
Error 1 Member 'System.Diagnostics.Process.Start(string, string)' cannot be accessed with an instance reference; qualify it with a type name instead
Joseph replied
to Robbe Morris at Saturday, October 20, 2007 12:10 PM
I get the same error with a much simpler project. I am trying to invoke the command prompt using mklink, however I cannot get it to work in C#. The following line throws the error:
p.Start("CMD.exe", "/C mklink /d " + "C:\\testphase" + i + " " + availableShadowCopies[i]);
I've already tried the following code, but it doesn't seem to be run by command prompt for some reason:
for(int i = 0; i < comboBox1.Items.Count; ++i)
{
Process p = new Process();
p.StartInfo.FileName = "CMD.exe";
p.StartInfo.Arguments = "mklink /d " + "C:\\testphase" + i + " " + availableShadowCopies[i];
p.StartInfo.CreateNoWindow = true;
p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
p.StartInfo.UseShellExecute = false;
p.StartInfo.RedirectStandardOutput = true;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
richTextBox1.Text += output;
}
Aman replied
to prabhu rsp at Saturday, October 20, 2007 12:10 PM
http://www.eggheadcafe.com/tutorials/aspnet/2a5222d8-3d69-4f1c-b5ab-35ca33da0f65/net-setup-deployment--m.aspx
I have read the article but I m not able to implement this code can u please help me regarding this as my teacher.
what I want to do in my project is I want to run SQLServer2005_SSMSEE.msi, sqlexpr32.exe and framework 2.0
Please help me regarding this