How can I programmatically read the properties inside an MSI file? - wix

Is there a way to read the properties inside an MSI file?
For example, given a MSI file named Testpackage.msi, I need to find
productName
PackageCode
version
This I am going to use it with WMI uninstall
string objPath = string.Format("Win32_Product.IdentifyingNumber='{0}', Name='{1}', Version='{2}'", "{AC9C1263-2BA8-4863-BE18-01232375CE42}", "testproduct", "10.0.0.0");
Using Orca is a great option, if this can be achieved programmatically. Then I can use this to generate automatic release notes. And in un-installing program too.

You can use the COM-based API for working with MSI, and do something like
Function GetVersion(ByVal msiName)
Const msiOpenDatabaseModeReadOnly = 0
Dim msi, db, view
Set msi = CreateObject("WindowsInstaller.Installer")
Set db = msi.OpenDataBase(msiName, msiOpenDatabaseModeReadOnly)
Set view = db.OpenView("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductVersion'")
Call view.Execute()
GetVersion = view.Fetch().StringData(1)
End Function

WiX toolset: WiX quick-start tips (collection of links to resources). WiX installs DTF.
I just want to mention that things have gotten even easier now. There is a full .NET wrapper for the Windows Installer object model, so you can avoid any COM interop clunkiness.
DTF - Getting Started: Main file: Microsoft.Deployment.WindowsInstaller.dll
Download and install the WiX toolkit
Find the files below in the WixInstallPath\SDK directory
The wrapper is called "Deployment Tools Foundation" (DTF) and here is the basic description: "Deployment Tools Foundation is a rich set of .NET class libraries and related resources that together bring the Windows deployment platform technologies into the .NET world. It is designed to greatly simplify deployment-related development tasks while still exposing the complete functionality of the underlying technology".
Here is a stripped-down, hands-on sample:
using (var db = new Database(FullPath, DatabaseOpenMode.ReadOnly))
{
PackageCode = db.SummaryInfo.RevisionNumber;
AppVendor = db.SummaryInfo.Author;
AppName = db.SummaryInfo.Title;
ProductName = db.SummaryInfo.Subject;
ProductCode = (string)db.ExecuteScalar("SELECT `Value` FROM "+
"`Property` WHERE `Property` = 'ProductCode'");
AppVersion = (string)db.ExecuteScalar("SELECT `Value` FROM "+
"`Property` WHERE `Property` = 'ProductVersion'");
UpgradeCode = (string)db.ExecuteScalar("SELECT `Value` FROM "+
" `Property` WHERE `Property` = 'UpgradeCode'");
}
Primary DTF files (the latter two are the most used ones):
Microsoft.Deployment.Compression.dll - Framework for archive packing and unpacking.
Microsoft.Deployment.Compression.Cab.dll - Implements cabinet archive packing and unpacking.
Microsoft.Deployment.Resources.dll - Classes for reading and writing resource data in executable files.
Microsoft.Deployment.WindowsInstaller.dll - Complete .NET based class library for the Windows Installer APIs.
Microsoft.Deployment.WindowsInstaller.Package.dll - Extended classes for working with Windows Installer installation and patch packages.
Just create a C# project, reference these files, and code your own deployment application with whatever control you desire and need. I am not set up with the tools for DTF at the moment, but see this sample for a general idea of how a C# program would work.
DTF is included with WIX. Download WiX from here.
The DTF dlls are in the SDK folder in the main WiX installation folder (the default location is: %ProgramFiles(x86)%\WiX Toolset v3.10\SDK). The version number will probably be different by the time you see this. Just look for the WiX folder under %ProgramFiles(x86)%.
Look for the DTF help files in the "doc" folder. DTF.chm and DTFAPI.chm. Absolutely excellent documentation for the object model and its usage.
See this serverfault.com post for some more DTF details
Some starter suggestions for working with WiX

You can use Microsoft's Orca.exe. Orca will allow you to open the MSI and edit/view all the tables in it. You will have to download the entire Windows SDK in order to get it, but thankfully that is free.
One alternative (which might be faster due to the download size of the SDK) is to use dark.exe from the WiX project. Dark is a MSI decompiler, which will export everything into an XML file and collection of resources. The XML it outputs will have the information you are looking for.

Here's a similar example in VBScript which I use as part of my build process in creating bootstrapper executables...
Option Explicit
Const MY_MSI = "product.msi"
Dim installer, database, view, result, sumInfo, sPackageCode
Set installer = CreateObject("WindowsInstaller.Installer")
Set database = installer.OpenDatabase (MY_MSI, 0)
Set sumInfo = installer.SummaryInformation(MY_MSI, 0)
sPackageCode = sumInfo.Property(9) ' PID_REVNUMBER = 9, contains the package code.
WScript.Echo "ProductVersion=" & getproperty("ProductVersion")
WScript.Echo "ProductCode=" & getproperty("ProductCode")
WScript.Echo "PackageCode=" & sPackageCode
WScript.Echo "ProductName=" & getproperty("ProductName")
Function getproperty(property)
Set view = database.OpenView ("SELECT Value FROM Property WHERE Property='" & property & "'")
view.Execute
Set result = view.Fetch
getproperty = result.StringData(1)
End Function

I found a lightweight non-programmatic solution in lessmsi. It apparently uses wix and just explodes the whole .msi into a specified folder. (It also has a UI but it didn't render great for me on Win7).

Related

Using QTP/UFT's DotNetFactory.dll in VBScript

I am trying to use QTP/UFT's DotNetFactory utility in standalone vbs files. I believe UFT is using DotNetFactory.dll found in the C:\Program Files (x86)\HP\Unified Functional Testing\bin to implement the feature. I have checked and found that the DLL is registered and its ComVisible property is also set to True. So effectively, I should be able to CreateObject to create an instance. However, VBScript (32-bit or 64-bit) is not able to create or get object for this dll.
None of the following is working
Set objDotNet = GetObject("C:\Program Files (x86)\HP\Unified Functional Testing\bin\DotNetFactory.dll")
Set objDotNet = GetObject(,"Mercury.DotNetFactory.1")
Set objDotNet = CreateObject("Mercury.QTP.Utils.DotNetFactory")
Set objDotNet = CreateObject("Mercury.DotNetFactory.1")

How to show a warning and exit if a software is not installed on WIX installer

I'm new with Wix, let me give some info about what I'm trying to accomplish. I have an installer for software "B" but this software needs to have software "A" to actually work. So I want to add on the installer for software "B" to check if software "A" is installed, if it isn't then to show a message and then exit the installer.
So below is the code of what I have tried but the message always shows even though the file is there. So I'm basically looking for a file from software "A", if its present then the install should continue normally, if it isn't then the warning message should be displayed and exit installer.
<Property Id="SOFTWARE_A_INSTALLED">
<DirectorySearch
Id="LocationFile"
Path="C:\Windows\Microsoft.NET\assembly\SOFTWAREA">
<FileSearch Name="A.dll"></FileSearch>
</DirectorySearch>
</Property>
<Condition Message="[ProductName] requires SOFTWARE A installed.">
<![CDATA[Installed OR SOFTWARE_A_INSTALLED]]>
</Condition>
Thanks
There are usually much better ways of doing this. If that product is an MSI file then use an Upgrade element to detect the product's UpgradeCode, or use the Component ID of that assembly to do a component search. Or maybe the product creates a registry key that you can search for.
In general I don't recommend your approach because you said that your product B requires A for it to work. You didn't say that your INSTALL requires A for the install to succeed, so you are creating an install order dependency when there is actually only a product dependency. So what does your product do if A is uninstalled? Crash? Give any kind of warning? A better solution might be for your app to do the check rather then create a required install order.
If that assembly from A is really a dependency of your app and it is just one of a few files you depend on, then maybe it should be a redistributable, available as something such as a merge module. People don't (for example) check if Crystal Reports files are on the system - they just include the merge module that includes the files and installs them in a way that multiple users of a system can all share the same files. The same is true of many other shared files.
This vbscript will enumerate installed component ids to check that you have the right values for a component search:
Option Explicit
Public installer, fullmsg, comp, a, prod, fso, pname, ploc, pid, psorce
Set fso = CreateObject("Scripting.FileSystemObject")
Set a = fso.CreateTextFile("comps.txt", True)
' Connect to Windows Installer object
Set installer = CreateObject("WindowsInstaller.Installer")
a.writeline ("MSI Components")
on error resume next
For Each comp In installer.components
a.writeline (comp & " is used by the product:")
for each prod in Installer.ComponentClients (comp)
pid = installer.componentpath (prod, comp)
pname = installer.productinfo (prod, "InstalledProductName")
a.Writeline (" " & pname & " " & prod & "and is installed at " & pid)
Next
Next

How to call DLL functions from InstallScript called by MSI custom action

I have a DLL named FSSetup.dll that I want to call from InstallScript code. And I want to call the InstallScript code from an MSI custom action. But there seems to be a problem in that I don't know where to find FSSetup.dll. The InstallScript function is starting and the whole install aborts when it gets to the line where it calls the DLL function. The MSI log reports:
MSI (c) (1C:3C) [09:39:52:261]: Invoking remote custom action. DLL: C:\Users\bmarty\AppData\Local\Temp\MSIBEAE.tmp, Entrypoint: f1
Action ended 9:39:55: BrowseFSKeyFile. Return value 3.
Info 2896. Executing action BrowseFSKeyFile failed.
Action ended 9:39:55: InstallWelcome. Return value 3.
The InstallScript code looks like this:
export prototype BrowseFSKeyFile(HWND);
prototype FSSetup.FSBrowseFile( BYVAL STRING, BYVAL STRING, BYVAL STRING, BYREF STRING );
function BrowseFSKeyFile(hMSI)
NUMBER nKeyFile;
STRING svKeyFile, szKPath, szKFile, szTemp, szTemp1;
begin
MessageBox("Hi", INFORMATION);
svKeyFile = "";
FSBrowseFile( "*.key", "key", "Please select a KEY File...", svKeyFile );
MessageBox("Bye", INFORMATION);
...
end;
I see the "Hi" message, but the install aborts before "Bye". I suspect it's because FSSetup.dll doesn't exist in my %temp% directory where MSIBEAE.tmp resides, which presumably represents the extracted name of the ISSetup.dll that was generated by the InstallScript compile.
Edit:
The MSI debugger is showing a different value for SUPPORTDIR than the InstallScript is seeing. FSSetup.dll exists in the SUPPORTDIR provided by MSI, but not in the one seen from InstallScript.
You are missing a call to UseDll (and UnUseDll). In order to call UseDll, you will need to know the location of the DLL in question. If you have added it to the support files of the MSI project, you will need to retrieve the Windows Installer Property SUPPORTDIR to find this location - that's where Christopher Painter's answer comes into play - and then call e.g. UseDll(szSupportDir ^ "FSSetup.dll") before calling FSBrowseFile.
The SUPPORTDIR behavior was changed about 9 years ago when fundamental design flaws of InstallScript were fixed in InstallShield 12.0. See:
InstallShield 12 Beta2
and
InstallScript, meet CustomActionData
and
Upgrading Projects to InstallShield 12 (search for SUPPORTDIR)
Basically you need to do call MsiGetProperty() to get SUPPORTDIR. Also you need to pass this to your deferred custom actions as CustomActionData.

WiX Custom Action - MSI copy itself

Can someone help me to build a custom action in MSI which will copy itself after successful installation to some X location.
I have already seen it can be done using .exe but I want to do it only with CA.DLL (C#) as this exe will be an overhead.
Here's an example VB script that will find an installed product by name and copy the cached copy of the MSI. This will work on Windows 7 and later, as the full MSI is cached and any embedded cab files remain in the MSI. You just get the MSI without the payload on older systems.
Dim installer, products, product, productCode
Set installer = Wscript.CreateObject("WindowsInstaller.Installer")
For Each productCode In installer.Products
If InStr(1, LCase(installer.ProductInfo(productCode, "ProductName")), LCase("My Product Name")) Then Exit For
Next
If IsEmpty(productCode) Then Wscript.Quit 2
Set products = installer.ProductsEx(productCode, "", 7)
filesys.copyFile products(0).InstallProperty("LocalPackage"), "c:\path\to\newcopy.msi"

Executable directory where application is running from?

I need to get the path (not the executable) where my application is running from:
System.AppDomain.CurrentDomain.BaseDirectory()
When I run the above statement with & "/images/image.jpg" on my local machine it works fine but when I install the application on another machine it says it cannot find the file and there is a lot of extra path information some.
I just need the directory of where the app is running. I am coding in VB.NET with Visual Studio 2008.
Thanks!
This is the first post on google so I thought I'd post different ways that are available and how they compare. Unfortunately I can't figure out how to create a table here, so it's an image. The code for each is below the image using fully qualified names.
My.Application.Info.DirectoryPath
Environment.CurrentDirectory
System.Windows.Forms.Application.StartupPath
AppDomain.CurrentDomain.BaseDirectory
System.Reflection.Assembly.GetExecutingAssembly.Location
System.Reflection.Assembly.GetExecutingAssembly.CodeBase
New System.UriBuilder(System.Reflection.Assembly.GetExecutingAssembly.CodeBase)
Path.GetDirectoryName(Uri.UnescapeDataString((New System.UriBuilder(System.Reflection.Assembly.GetExecutingAssembly.CodeBase).Path)))
Uri.UnescapeDataString((New System.UriBuilder(System.Reflection.Assembly.GetExecutingAssembly.CodeBase).Path))
---
Edit October 18, 2021:
Sigh... None of the above work if using net5.0 or net6.0 and publishing app as single-file bundle. Best I got now is:
// This will give you the directory but not the assembly
string basedir = AppContext.BaseDirectory;
// Before you package the app as a single file bundle, you will get the dll.
// But after you publish it, you'll get the exe.
string pathToExecutable = Environment.GetCommandLineArgs()[0].Replace(".dll", ".exe");
Dim strPath As String = System.IO.Path.GetDirectoryName( _
System.Reflection.Assembly.GetExecutingAssembly().CodeBase)
Taken from HOW TO: Determine the Executing Application's Path (MSDN)
I needed to know this and came here, before I remembered the Environment class.
In case anyone else had this issue, just use this: Environment.CurrentDirectory.
Example:
Dim dataDirectory As String = String.Format("{0}\Data\", Environment.CurrentDirectory)
When run from Visual Studio in debug mode yeilds:
C:\Development\solution folder\application folder\bin\debug
This is the exact behaviour I needed, and its simple and straightforward enough.
Dim P As String = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase)
P = New Uri(P).LocalPath
You could use the static StartupPath property of the Application class.
You can write the following:
Path.Combine(Path.GetParentDirectory(GetType(MyClass).Assembly.Location), "Images\image.jpg")