|
Introduction
A project of mine required a setup application which could handle installation of Microsoft® Windows Installer .MSI packages using a custom user interface. The interface would be able to handle progress messages, etc. from the Windows Installer Service. There was one solution at Youseful, however it wasn't a complete enough solution for my needs.
So I wrote my own wrapper, and this article describes how to use it.
This article will not describe the full Windows Installer API, nor the nuances involved in its use, nor .NET Interop.
Please refer to the Microsoft® Windows Installer Reference for further information. The accompanying source code contains comments from the MSDN library, but it is far from a complete guide.
The accompanying source code is distributed under the GNU Lesser License Version 2.1.
The Wrapper
The Windows Installer Wrapper provided here, wraps all API calls in the MsiInterop class in the WindowsInstaller namespace. Supporting structures, delegates, constants and enumerations are also provided. The interop class as well as these constructs are marked internal, the rational being the contents of this namespace are meant to be used within an assembly, perhaps wrapped by a publicly visible object. Of course, you are free to change the namespace as you see fit.
There was a period of trial and error in defining just what the interop signatures would be, and I had to tweak and rebuild (along with some UI test harnesses) to get things to work happily. No doubt there will be more tweaking in the future as people use the wrapper.
All Win32 HANDLES are IntPtrs in the wrapper.
Where possible, the return values are MsiErrors, which map to both MSI-specific as well as Win32 error codes.
Constants are also provided for the MSI database tables (in the MsiDatabaseTable class) as well as Windows Installer properties (in the MsiInstallerProperty class.)
It should be noted that this wrapper is intended for systems running Microsoft® Windows® 2000 or higher, with Windows Installer 2.0 installed.
Using MsiInterop for Custom UI Progress Messages
In order for you to circumvent the Windows Installer internal user interface, you must first disable it using a call to MsiInterop.MsiSetInternalUI, then tell the service about your own, by calling MsiInterop.MsiSetExternalUI, providing it with your MsiInstallUIHandler delegate for handling the UI messages.
The following code describes an "external UI" scenario, using code below (see The Delegate): IntPtr parent = IntPtr.Zero;
MsiInstallUILevel oldLevel =
MsiInterop.MsiSetInternalUI(MsiInstallUILevel.None |
MsiInstallUILevel.SourceResOnly, ref parent);
MsiInstallUIHandler oldHandler = null;
try
{
oldHandler =
MsiInterop.MsiSetExternalUI(new
MsiInstallUIHandler(_OnExternalUI),
MsiInstallLogMode.ExternalUI, IntPtr.Zero);
Application.DoEvents();
MsiError ret =
MsiInterop.MsiInstallProduct(,
);
if (ret != MsiError.Success)
throw new
ApplicationException(string.Format("Failed to install -- {0}", ret));
}
catch (Exception e)
{
Debug.WriteLine("EXCEPTION -- " + e.ToString());
}
finally
{
if (oldHandler != null)
MsiInterop.MsiSetExternalUI(oldHandler,
MsiInstallLogMode.None, IntPtr.Zero);
MsiInterop.MsiSetInternalUI(oldLevel, ref parent);
}
Notice how a try/catch/finally is used to ensure that we clean up after ourselves!
The MsiInstallLogMode.ExternalUI is provided in the wrapper source code as a convenient enumeration value, for commonly-used logging modes for external user interfaces. Of course, you use and roll your own bitwise-OR*ed MsiInstallLogMode value, however it should be noted that the MsiInstallLogMode.ResolveSource cannot be handled by an external UI; the delegate code below handles it properly be returning 0, indicating the external UI did not handle the request.
The delegate used for callbacks from the Windows Installer API has the following signature: internal delegate int MsiInstallUIHandler(IntPtr context,
uint messageType, [MarshalAs(UnmanagedType.LPTStr)] string message);
Your call to MsiInterop.SetExternalUI can provide a context which can be used to help you with UI state. This context can be extracted using the Marshal.PtrToStructure or a similar method (assuming you used something like Marshal.StructureToPtr to create the beast).
The following code shows an example MsiInstallUIHandler: private int _OnExternalUI(IntPtr context, uint messageType, string message)
{
MsiInstallMessage msg =
(MsiInstallMessage)(MsiInterop.MessageTypeMask & messageType);
Debug.WriteLine(string.Format("MSI: {0} {1}", msg, message));
try
{
switch (msg)
{
case MsiInstallMessage.ActionData:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.ActionStart:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.CommonData:
string[] data = _ParseCommonData(message);
if (data != null && data[0] != null)
{
switch (data[0][0])
{
case '0':
break;
case '1':
break;
case '2':
if ("0" == data[1])
else
break;
default: break;
}
}
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.Error:
return (int)MessageBox.Show(message,
"Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
case MsiInstallMessage.FatalExit:
return (int)MessageBox.Show(message,
"Fatal Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
case MsiInstallMessage.FilesInUse:
Application.DoEvents();
return 0;
case MsiInstallMessage.Info:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.Initialize:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.OutOfDiskSpace:
Application.DoEvents();
break;
case MsiInstallMessage.Progress:
string[] fields = _ParseProgressString(message);
if (null == fields || null == fields[0])
{
Application.DoEvents();
return (int)DialogResult.OK;
}
switch (fields[0][0])
{
case '0':
break;
case '1':
break;
case '2':
break;
default: break;
}
Application.DoEvents();
if ()
return (int)DialogResult.Cancel;
else
return (int)DialogResult.OK;
case MsiInstallMessage.ResolveSource:
Application.DoEvents();
return 0;
case MsiInstallMessage.ShowDialog:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.Terminate:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.User:
Application.DoEvents();
return (int)DialogResult.OK;
case MsiInstallMessage.Warning:
return (int)MessageBox.Show(message,
"Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
default: break;
}
}
catch (Exception e)
{
Debug.WriteLine("EXCEPTION -- " + e.ToString());
}
Application.DoEvents();
return 0;
}
In order to get what MsiInstallMessage type the message is, you have to bitwise-AND it with MsiInterop.MessageTypeMask as shown above. The switch block handles the individual message types.
We wrap the whole activity in a try/catch block, to ensure we let the service run through, returning 0 to let it know we didn't handle the problem. If an exception gets thrown, the service fails the installation activity.
The spurious Application.DoEvents in there allows your application's message pump to run, and the return of the DialogResults tells the service that the message was handled. The functions to crack the message for MsiInstallMessage.CommonData and MsiInstallMessage.Progress are below: private string[] _ParseCommonData(string s)
{
string[] res = new string[3];
Regex regex = new Regex(@"\d:\w+\s");
int i = 0;
foreach (Match m in regex.Matches(s))
{
if (i > 3) return null;
res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 1).Trim();
}
return res;
}
private string[] _ParseProgressString(string s)
{
string[] res = new string[4];
Regex regex = new Regex(@"\d:\s\d+\s");
int i = 0;
foreach (Match m in regex.Matches(s))
{
if (i > 4) return null;
res[i++] = m.Value.Substring(m.Value.IndexOf(":") + 2).Trim();
}
return res;
}
The actual meanings of these "cracked" messages can be derived by referring to the Parsing Windows Installer Messages in the MSDN library.
Other Uses for MsiInterop
Since I've wrapped the complete (well, as complete as I can muster) Windows Installer API, the API becomes quite useful in the .NET world. You may find an interesting use, or even a problem with my wrapper! Let me know! It's my hope that this code will be useful.
Workspace
This code is maintained in a GotDotNet workspace.
References
Please feel free to browse the following reference material:
History
Revisions:
- 2004-01-05: Initial revision.
- 2004-01-06: Slight code revision.
| You must Sign In to use this message board. |
|
| | Msgs 1 to 19 of 19 (Total in Forum: 19) (Refresh) | FirstPrevNext |
|
|
 |
|
|
Is there any possibility to add new files to a msi installer? For Example I have created MyApp.msi ... but I want to add a different configuration file for each client. So I wonder if it is possible somehow to add or replace a file in the msi package without recreating the package. Thanks a lot.
http://www.softrun.ro
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
First of all, I wanted to say that the code of yours if very usefuel . But I do have an open question, I cann see in your code that you disable and enable a Cancel-Button where the user can cancel and rollback the installation. But what code is executed on the Click Event of the Button? Can anybody help me with this issue?
Thx
“Never mistake knowledge for wisdom. One may come from the other, but one helps you make a living, the other helps you make a life” Sandra Carey
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
My problem is that I have to install, uninstall msi files on a system programatically. The install may be quiet or with the installer UI. Also it should have the ability to install patches to installed MSIs programatically. Can you help? 
--- With best regards, A Manchester United Fan
The Genius of a true fool is that he can mess up a foolproof plan!
|
| Sign In·View Thread·PermaLink | 1.80/5 (2 votes) |
|
|
|
 |
|
|
Hi Friend, to install and uninstall Programmaticly i have a block of code all u have to pass the path of MSI to install and uninstall.
 There are some things that we must know "msiexec.exe" is a component of "Windows Installer" used to install, uninstall and repair software in "*.msi".
There are many options to manipulate the process, but we are just going to talk about two of them:
1. Setup Options (Typed before the path of your *.msi file)
* To install you just have to type "/i". * To uninstall you just have to type "/x". * To repair you just have to type "/f".
2. Screen Options (Typed after the path of your *.msi file)
* If you don't want to display a user interface "/qn". * To display a reduced user interface with a modal dialog box displayed at the end of the installation "/qb". * To display the full user interface with a modal dialog box displayed at the end "/qr". * To display no user interface, except for a modal dialog box displayed at the end "/qf".
Note: uninstallation can be done by product code as well Here is 3 method whice perform all action like Install,uninstall,Repair
using System.Diagnostics; The code to install software with no user interface is: private void installSoftware() { Process p = new Process(); p.StartInfo.FileName = "msiexec.exe"; p.StartInfo.Arguments = "/i \"C:\\Application.msi\"/qn"; p.Start(); } The code to uninstall software with no user interface is: private void uninstallSoftware() { Process p = new Process(); p.StartInfo.FileName = "msiexec.exe"; p.StartInfo.Arguments = "/x \"C:\\Application.msi\"/qn"; p.Start(); } The code to repair software with no user interface is:
private void repairSoftware() { Process p = new Process(); p.StartInfo.FileName = "msiexec.exe"; p.StartInfo.Arguments = "/f \"C:\\Application.msi\"/qn"; p.Start(); }
at the end
Regards,
Lalit Narayan Dubey S/W Engineer,Singapore
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
please replace with following code
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiRecordGetString(IntPtr record, uint field, StringBuilder value, ref uint valueSize);
usage example:
l_Builder = new StringBuilder(2048, 2048); l_ValueSize = 2048; l_Error = MsiInterop.MsiRecordGetString (l_RecordPtr, 5, l_Builder, ref l_ValueSize); string l_Version = l_Builder.ToString();
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
This actually would be applicable to any of the Interop Methods where a string value is returned w/a value size (unfortunately, I cannot reupload the file so you'll have to make the mods yourself):
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiEnumComponentQualifiers(string component, uint index, StringBuilder qualifier, ref uint qualifierSize, StringBuilder appData, ref int appDataSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiEnumPatches(string product, uint index, string patch, StringBuilder transform, ref uint transformSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiInstallState MsiGetComponentPath(string product, string component, StringBuilder path, ref uint pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetFeatureInfo(IntPtr productHandle, string feature, MsiInstallFeatureAttribute attributes, StringBuilder title, ref uint titleSize, StringBuilder help, ref uint helpSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetFileVersion(string path, StringBuilder version, ref uint versionSize, StringBuilder language, ref uint languageSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetPatchInfo(string patch, string attribute, StringBuilder value, ref uint valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetProductInfo(string product, string property, StringBuilder value, ref uint valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetProductInfoFromScript(string scriptFile, string product, ref UInt16 langId, ref uint version, StringBuilder name, ref uint nameSize, StringBuilder package, ref uint packageSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetProductProperty(IntPtr productHandle, string property, StringBuilder value, ref uint valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiUserInfoState MsiGetUserInfo(string product, StringBuilder user, ref uint userSize, StringBuilder org, ref uint orgSize, StringBuilder serial, ref uint serialSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiInstallState MsiLocateComponent(string component, StringBuilder path, ref uint pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public uint MsiProvideComponent(string product, string feature, string component, uint mode, StringBuilder path, ref int pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiProvideQualifiedComponent(string component, string qualifier, uint mode, StringBuilder path, ref int pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiProvideQualifiedComponentEx(string component, string qualifier, uint mode, string product, uint unused1, uint unused2, StringBuilder path, ref int pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiFormatRecord(IntPtr install, IntPtr record, StringBuilder result, ref uint resultSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetProperty(IntPtr install, string name, StringBuilder value, ref uint valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetSourcePath(IntPtr install, string folder, StringBuilder path, ref uint pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiGetTargetPath(IntPtr install, string folder, StringBuilder path, ref uint pathSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiRecordGetString(IntPtr record, uint field, StringBuilder value, ref uint valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiRecordReadStream(IntPtr record, uint field, StringBuilder buffer, ref uint bufferSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiError MsiSummaryInfoGetProperty(IntPtr summaryInfo, uint id, out VariantType type, out int intValue, out FILETIME fileTime, StringBuilder value, ref int valueSize);
[DllImport(MSI_LIB, CharSet = CharSet.Auto)] extern static public MsiDbError MsiViewGetError(IntPtr view, StringBuilder columnNames, ref uint columnNamesSize);
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I have simple ExtrnalUI monitering like following,
--------------8<----------------- { : : oldHandler = MsiInterop.MsiSetExternalUI(new MsiInstallUIHandler(_OnExternalUI), MsiInstallLogMode.ExternalUI, IntPtr.Zero);
MsiError ret = MsiInterop.MsiInstallProduct(@"SomeInstall.msi", ""); : : } private int _OnExternalUI(IntPtr context, uint messageType, string message) { MsiInstallMessage msg = (MsiInstallMessage)(MsiInterop.MessageTypeMask & messageType); Console.WriteLine(string.Format("MSI: {0} {1}", msg, message)); return 0; } --------------8<-----------------
Now whenever I run this, I start receiving the messages, but I get "NullReferenceException" at MsiInterop.MsiInstallProduct(@"SomeInstall.msi", "") half way through. It is not consisitant when/where. But it just dies, and it always does it. I have checked "SomeInstall.msi" for install problems, "SomeInstall.msi" works fine otherwise without any problems.
Any pointer/help will be apprecitated.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Tough, right?
Do you have a dump or at least a call stack?
Here I can tell you one possiblity.
This AV most likely happens when msi.dll calls back your UI handler. Call stack should look like this,
!DomainBoundILStubClass.IL_STUB(Int64, Int32, IntPtr) mscorwks!UMThunkStub... msi!CMsiAPIMessage::Message
You don't know what happens, since it is in the native code stub generated by .net, not your code.
The problem is there is a bug in the posted code. Of course it is not the only one. The delegate lifetime is too short. GC will collect it while msi.dll still tries to call back, but the stub cannot find the call back function pointer through the delegate.
oldHandler = MsiInterop.MsiSetExternalUI(new MsiInstallUIHandler(_OnExternalUI), MsiInstallLogMode.ExternalUI, IntPtr.Zero);
Now the rest is simple, move the 'new MsiInstallUIHandler(_OnExternalUI)' out of this function.
How would you appreciate me?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
If anybody need to get better solution please send to me a letter. My e-mail - valera_tyt@list.ru.
I'll send you help file. What advantage you will recieve: 1. All code organized as a set of classes which easy to use. 2. Using session object to change or recieve any property in package. 3. All classes supports IDisposable interface and you can use using(SomeClass someClass = new SomeClass){....} instraction. Any unmanaged code will be released automatically. 4. Every method has fully documentation description. 5. You can develop custom appearance of setup application fast. It is a advantage over your competitor.
And so on. If you interested in it I will answer to your letter.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
This sounds like what I need, but I also need some advice. What if I have another installer application that is not in MSI format? Will this work? I'm trying to do some thing like the Flight1 Wrapper, where I'm given an installer to wrap, but run a user registration before allowing the software to install.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
For me the RegEx in _ParseCommonData did not work. I changed ist from Regex(@"\d:\w+\s") to Regex(@"\d:\s?\w+\s"). Now there may be a space behind the ":", or not.
Besides, the article helped me much.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
First off, nice work man....
Curious if you (or anyone reading this thread) has run across a solution for the below:
I've read lot's of stuff regarding external [custom] UI with windows installer, but all that I've come across stops at the initial UI sequence. Most installations have "maintenance mode" UI where the user can chose to Add / Remove features etc....
So my question is this: How does one utilize the external [custom] UI during maintenance mode? (keeping in mind the installer caching of the MSI, that users access maintenance mode from control panel -> Add Remove Programs, not by hunting down the custom UI application which then might invoke the installation and interact with it during maintenance ops).
Is there a way to package the custom UI within the MSI in such a way that when the installation is invoked, itself invokes the custom UI? (that then might get cached inside the local copy, etc... for use in maintenance mode?)
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I am not 100% about this but the windows installer add a registry entry to the windows registry
for uninstall information (some where in HKEY_LOCAL_MACHINE) so if you keep a copy of the
installer and you custom UI in the Application Folder you can modify that registry entry so it
points to your installed custom interface exe which will run and perform maintences ( maybe
use command line options to tell the difference between install, uninstall, maintence etc).
Modify this registry entry after the MSI has finshed using the custom interface which is easy
enough.
Hope this fixes your problem
(sorry for any spelling mistakes )
Lloyd Sparkes
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Sure would be nice to get an example of how to use this. I was easily able to use it for opening and interrogating an MSI, but I dont have any clue how to access the current Session Object or where you start for replacing the MSI GUI install... I'm sure I'm missing something very basic here.
Do you have to start the Install yourself in order to replace the GUI, or can it be done from a callout of an MSI ?
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
You would replace the GUI by using your own setup program. You use this wrapper from the custom setup program to start the install yourself. See the "Using MsiInterop for Custom UI Progress Messages" section in my article. Once you set up the custom UI handling, you'll call MsiInterop.MsiInstallProduct() to launch an installation. You can pass command line arguments to perform/pass properties to the installer engine.
You should refer to the Windows Installer Dox on MSDN for more information on passing args to the engine. As a quick example, you can pass an MSI "property" on the command line by using PROPERTY=setting, e.g., ADDLOCAL=myFeature,anotherFeature ADVERTISE=myAdvertisedFeature would install myFeature and anotherFeature onto the local hard drive and advertise myAdvertisedFeature for installation on first use. If you pass REMOVE=ALL, the "the installer removes all features having an install level greater than 0."
I've used my MsiInterop to do a custom installer which queries a setup MSI for installed/uninstalled portions of a product; allows the user to modify the installation using a tree view; and passes the options on the command line in MsiInterop.MsiInstallProduct().
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Thanks, hopefully I will be able to get a prototype up and running. So is there anyway to use to use this from a standard MSI install to interact with the Installation Process, seems like this should be possible too ?
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I was unable to get it working as suggested here, but with a few modifications it looks to be working. I was unable to access my Controls form during the events but by running the install on a background thread I had no problems.
What I haven't figured out is how to get any meaningful percent complete ? Following the comments in the source I ended up with "progress" numbers ranging from 0 to many thousands, with no pattern recognizable at first glance.
Were you able to calculate a reliable percent complete ?
PS,
I REALLY appreciate this posting, as our installs are able to be make to look MUCH better now.
Mike
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I found the documentation in the MSDN for processing this, looks like it should be no problem. Still perplexed as to how I can access the Session Object to interact with the install though, my goal is to have active GUI in the same .NET app.
Mike
|
| Sign In·View Thread·PermaLink | 5.00/5 (1 vote) |
|
|
|
 |
|
|
I've also checked a launch condition exists to allow installation. This condition is passed to the MSI I'm going to install. This for the most part prevents someone from just launching it.
If you want to go even further, try Bootstrapping
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
General News Question Answer Joke Rant Admin
|