0
votes

Stemming from an idea presented in Variable ODBC DSN name in InstallShield MSI installation, I am attempting to find the best way to define registry entries in a multi-instance MSI installer project created with InstallShield 2015 such that they clean up after their own instance is un-installed. To test the idea I have followed these steps:

  1. Create a new Basic MSI project called Dummy1.
  2. In Project assistant on Application Files step, add Readme.txt as a file to deliver to [ProgramFilesFolder]\My Company Name\My Product Name.
  3. In Installation Designer, Files and Folders, right-click and set it as Key File.
  4. In Components view, add registry entry HKLM\SOFTWARE\Dummy, Name [InstanceId], Value "Installed".
  5. Use Release Wizard to create a Release with all the default values.
  6. On Product Configuration 1, select Multiple Instances tab and add Instance 1.
  7. In Project Assistant, Application Interview, set property to "Yes" to allow specifying install location.
  8. Build setup.exe and run it twice from Windows Explorer, installing to Company\1 first and then Company\2.
  9. Verify HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Dummy contains "0" and "1" values.
  10. Un-install one instance of Dummy1 from control panel.
  11. Observe that one of the README.TXT files has been removed, but the empty directory containing it ("1" in my case), and the registry entries are all still there.

I would have expected in step 11 that the directory that was created and one of the registry entries would have been removed. Did I miss something?

Edit: When I turn on lots of logging I can see a message like this:

MSI (s) (E0:F8) [14:30:24:540]: Allowing uninstallation of shared component: {3AACE297-A264-4223-A0AF-C5A20D37551F}. Other clients exist, but installed to a different location

It appears at least in IS 2015 there's no similar recognition of registry entries installing to different paths included in a "shared" component, and the registry entries in the same component as a file that's being removed do not seem to be rolled up in the removal of the component.

I can see in the log file for the removal of the final component installation:

MSI (s) (E0:08) [14:36:09:538]: Executing op: ActionStart(Name=RemoveRegistryValues,Description=Removing system registry values,Template=Key: [1], Name: [2])
MSI (s) (E0:08) [14:36:09:538]: Executing op: ProgressTotal(Total=1,Type=1,ByteEquivalent=13200)
MSI (s) (E0:08) [14:36:09:538]: Executing op: RegOpenKey(Root=-2147483646,Key=SOFTWARE\Infor\0,,BinaryType=0,,)
MSI (s) (E0:08) [14:36:09:538]: Executing op: RegRemoveValue(Name=Test,Value=One,)

And it does not mention "Allowing uninstallation of shared component" as I saw in the log for the uninstallation of the duplicated component. And, in the log for the uninstallation of the duplicated component, all 4 of these lines are simply absent and I don't know why.

1

1 Answers

0
votes

There is an elegant solution that took quite a bit of effort to work out being someone not very familiar with Windows Installer or InstallShield. The Events tab of the Release object on InstallShield's Releases page has a Precompression event, during which time it's possible to modify the MSI file before it gets compressed into Setup.exe. I have entered this command into the Precompression event:

"<ISProjectFolder>\UpdateMSI.exe" "<ISReleasePath>\<ISProductConfigName>\<ISReleaseName>\DiskImages\Disk1\Dummy.msi"

Where Dummy.msi is the msi generated by InstallShield before it gets compressed (or if the release is configured not to compress the result). I created a VB.NET program called UpdateMSI and put the output in C:\InstallShield 2015 Projects\. What this program does is essentially extract all the instance transforms generated and embedded into the MSI file by InstallShield, and updates them to also update the GUIDs for the component(s). Right now I have only one component which updates the registry so I am updating just the one component to have a separate GUID for each instance (up to 9 instances plus the default instance).

The result is a setup program that cleanly installs and un-installs these components by retaining separate GUIDs for them without having to duplicate them in the InstallShield project.

The code for UpdateMSI looks like this.

Imports System.Runtime.InteropServices

Module Main
   Private Declare Auto Function MsiOpenDatabase Lib "msi.dll" (ByVal szDatabasePath As String, ByVal szPersist As IntPtr, <Out> ByRef phDatabase As IntPtr) As UInt32
   Private Declare Auto Function MsiCloseHandle Lib "msi.dll" (ByVal hAny As Integer) As UInt32
   Private Declare Auto Function MsiDatabaseOpenView Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal szQuery As String, <Out> ByRef phView As IntPtr) As UInt32
   Private Declare Auto Function MsiViewExecute Lib "msi.dll" (ByVal hView As IntPtr, ByVal hRecord As IntPtr) As UInt32
   Private Declare Auto Function MsiViewFetch Lib "msi.dll" (ByVal hView As IntPtr, <Out> ByRef phRecord As IntPtr) As UInt32
   Private Declare Auto Function MsiRecordGetString Lib "msi.dll" (ByVal hRecord As IntPtr, iField As UInt32, ByVal szValueBuf As System.Text.StringBuilder, ByRef pcchValueBuf As UInt32) As UInt32
   Private Declare Auto Function MsiViewModify Lib "msi.dll" (ByVal hView As IntPtr, eModifyMode As IntPtr, hRecord As IntPtr) As UInt32
   Private Declare Auto Function MsiRecordSetString Lib "msi.dll" (ByVal hRecord As IntPtr, iField As UInt32, szValue As String) As UInt32
   Private Declare Auto Function MsiDatabaseCommit Lib "msi.dll" (ByVal hDatabase As IntPtr) As UInt32
   Private Declare Auto Function MsiViewClose Lib "msi.dll" (ByVal hView As IntPtr) As UInt32
   Private Declare Auto Function MsiDatabaseApplyTransform Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal szTransformFile As String, ByVal iErrorConditions As Integer) As UInt32
   Private Declare Auto Function MsiDatabaseGenerateTransform Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal hDatabaseReference As IntPtr, ByVal szTransformFile As String, ByVal iReserved As Integer, ByVal iReserved As Integer) As UInt32
   Private Declare Auto Function MsiCreateTransformSummaryInfo Lib "msi.dll" (ByVal hDatabase As IntPtr, ByVal hDatabaseReference As IntPtr, ByVal szTransformFile As String, ByVal iErrorConditions As Integer, ByVal iValidation As Integer) As UInt32
   Private Declare Auto Function MsiRecordSetStream Lib "msi.dll" (ByVal hRecord As IntPtr, ByVal iField As UInt32, ByVal szFilePath As String) As UInt32

   Private Const MSIDBOPEN_READONLY As Integer = 0
   Private Const MSIDBOPEN_TRANSACT As Integer = 1
   Private Const MSIDBOPEN_DIRECT As Integer = 2
   Private Const ERROR_SUCCESS As Integer = 0
   Private Const ERROR_NO_MORE_ITEMS As Integer = 259
   Private Const MSIMODIFY_UPDATE As Integer = 2

   Private Enum MSITRANSFORM_VALIDATE
      None = 0
      Language = 1
      Product = 2
      MajorVersion = 8
      MinorVersion = &H10
      UpdateVersion = &H20
      NewLessBaseVersion = &H40
      NewLessEqualBaseVersion = &H80
      NewEqualBaseVersion = &H100
      NewGreaterEqualBaseVersion = &H200
      NewGreaterBaseVersion = &H400
      UpgradeCode = &H800
   End Enum

   Const RegComponentGuid As String = "{3AACE297-A264-4223-A0AF-C5A20D37551F}"
   Dim RegComponentInstGuids As String() = { _
      "{12456A37-51F3-4F53-A19E-34DF8CB1B063}", _
      "{0F00D476-E1F2-4B5D-85C4-D380A33BB78E}", _
      "{EFC3C9D6-9C2B-43E9-84A3-837C86BE5DD8}", _
      "{1398A80D-681D-4726-9972-8DEC8B030B4A}", _
      "{FF610463-5E35-4059-A3C4-EB51A7CABA1D}", _
      "{C4A43C73-5791-4B17-906C-93240AAB2F03}", _
      "{12E37949-704F-4A92-A7D2-5B7B94554505}", _
      "{E9175D7C-BC4E-4AC1-A79D-6B314EAB5D45}", _
      "{0A1E0D5C-44CC-4199-9C4E-FE23A1136B60}"}

   Function Main(args As String()) As Integer
      If args.Count <> 1 Then
         Console.Error.WriteLine(String.Format("{0} <MSI File>", System.Reflection.Assembly.GetExecutingAssembly().GetName().Name))
         Console.ReadLine()
         Return 1
      End If
      Main = UpdateTransforms(args(0))
   End Function

   Public Sub CheckResult(result As Integer, Optional ByVal failureName As String = "MSI call")
      If result <> ERROR_SUCCESS Then
         Throw New ApplicationException(String.Format("{0} failed with code {1}", failureName, result))
      End If
   End Sub

   Private Function UpdateTransforms(msiFile As String)
      Dim hDB As IntPtr = IntPtr.Zero
      Dim hDBOriginal As IntPtr = IntPtr.Zero
      Dim hView As IntPtr = IntPtr.Zero
      Dim mainResult As Integer = 0
      Dim hRec As IntPtr = IntPtr.Zero
      Dim instanceNames As IEnumerable(Of String) = Nothing

      Try
         instanceNames = GetEmbeddedTransforms(msiFile)
         Console.WriteLine("Found {0} instances embedded in {1}", instanceNames.Count, msiFile)
         If instanceNames.Count > RegComponentInstGuids.Length Then
            Console.Error.WriteLine("More instances were found than pre-defined GUIDs")
            Console.ReadLine()
            Return 4
         End If

         ' For each embedded instance, modify the MST for that instance to also modify component GUIDs
         Dim componentIndex As Integer = 0
         For Each instanceName In instanceNames
            Dim msiCopy As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "InstanceIdTmp.msi")
            System.IO.File.Copy(msiFile, msiCopy)
            CheckResult(MsiOpenDatabase(msiCopy, MSIDBOPEN_TRANSACT, hDB), "MsiOpenDatabase")
            CheckResult(MsiDatabaseApplyTransform(hDB, String.Format(":{0}", instanceName), 0), "MsiDatabaseApplyTransform")
            CheckResult(MsiDatabaseOpenView(hDB, String.Format("UPDATE `Component` SET `Component`.`ComponentId`='{0}' WHERE `Component`.`ComponentId`='{1}'", RegComponentInstGuids(componentIndex), RegComponentGuid), hView), "MsiDatabaseOpenView")
            CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
            CheckResult(MsiViewClose(hView), "MsiViewClose")
            CheckResult(MsiDatabaseCommit(hDB), "MsiDatabaseCommit")
            CheckResult(MsiOpenDatabase(msiFile, MSIDBOPEN_TRANSACT, hDBOriginal), "MsiOpenDatabase")
            Dim mstFile As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), instanceName)
            CheckResult(MsiDatabaseGenerateTransform(hDB, hDBOriginal, mstFile, 0, 0), "MsiDatabaseGenerateTransform")
            CheckResult(MsiCreateTransformSummaryInfo(hDB, hDBOriginal, mstFile, 0, MSITRANSFORM_VALIDATE.UpgradeCode), "MsiCreateTransformSummaryInfo")
            CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
            hView = IntPtr.Zero
            CheckResult(MsiDatabaseOpenView(hDBOriginal, String.Format("SELECT Data FROM `_Storages` WHERE Name='{0}'", instanceName), hView), "MsiDatabaseOpenView")
            Console.WriteLine("Updating component {0} in instance {1} to use {2}", RegComponentGuid, componentIndex, RegComponentInstGuids(componentIndex))
            CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
            CheckResult(MsiViewFetch(hView, hRec), "MsiViewFetch")
            CheckResult(MsiRecordSetStream(hRec, 1, mstFile), "MsiRecordSetStream")
            CheckResult(MsiViewModify(hView, 2, hRec), "MsiViewModify")
            CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
            hRec = IntPtr.Zero
            CheckResult(MsiViewClose(hView), "MsiViewClose")
            CheckResult(MsiDatabaseCommit(hDBOriginal), "MsiDatabaseCommit")
            If System.IO.File.Exists(mstFile) Then System.IO.File.Delete(mstFile)
            CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
            hView = IntPtr.Zero
            CheckResult(MsiCloseHandle(hDBOriginal), "MsiCloseHandle")
            hDBOriginal = IntPtr.Zero
            CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
            hDB = IntPtr.Zero
            System.IO.File.Delete(msiCopy)
            componentIndex += 1
         Next
      Catch ex As System.Exception
         Console.Error.WriteLine(ex.ToString())
         Console.ReadLine()
         mainResult = 2
      Finally
         Try
            If hRec <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
            If hDBOriginal <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hDBOriginal), "MsiCloseHandle")
            If hView <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
         Catch ex As Exception
            Console.Error.WriteLine(ex.ToString())
            Console.ReadLine()
            mainResult = 3
         Finally
            If hDB <> IntPtr.Zero Then
               CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
            End If
            Dim msiCopy As String = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "InstanceIdTmp.msi")
            If System.IO.File.Exists(msiCopy) Then System.IO.File.Delete(msiCopy)
            If instanceNames IsNot Nothing Then
               For Each instanceName In instanceNames
                  If System.IO.File.Exists(instanceName) Then System.IO.File.Delete(instanceName)
               Next
            End If
         End Try
      End Try
      Return mainResult
   End Function

   Private Function GetEmbeddedTransforms(msiFile As String) As IEnumerable(Of String)
      Dim hDB As IntPtr = IntPtr.Zero
      Dim hView As IntPtr = IntPtr.Zero
      Dim hRec As IntPtr = IntPtr.Zero
      Dim instanceNames As New LinkedList(Of String)
      Try
         CheckResult(MsiOpenDatabase(msiFile, MSIDBOPEN_READONLY, hDB), "MsiOpenDatabase")
         CheckResult(MsiDatabaseOpenView(hDB, "SELECT Name FROM `_Storages`", hView), "MsiDatabaseOpenView")
         CheckResult(MsiViewExecute(hView, IntPtr.Zero), "MsiViewExecute")
         Do
            Dim fetchResult = MsiViewFetch(hView, hRec)
            If fetchResult = ERROR_NO_MORE_ITEMS Then Exit Do
            CheckResult(fetchResult)
            Dim instMstName As New System.Text.StringBuilder(256)
            CheckResult(MsiRecordGetString(hRec, 1, instMstName, instMstName.Capacity - 1), "MsiRecordGetString")
            If instMstName.ToString() Like "InstanceId#*.mst" Then
               instanceNames.AddLast(instMstName.ToString())
            End If
            CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
            hRec = IntPtr.Zero
         Loop
         CheckResult(MsiViewClose(hView), "MsiViewClose")
      Catch ex As System.Exception
         Console.Error.WriteLine(ex.ToString())
         Console.ReadLine()
      Finally
         If hRec <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hRec), "MsiCloseHandle")
         If hView <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hView), "MsiCloseHandle")
         If hDB <> IntPtr.Zero Then CheckResult(MsiCloseHandle(hDB), "MsiCloseHandle")
      End Try
      Return instanceNames
   End Function
End Module