Question

I'm trying to find if an app is installed, and what the installed path is. I have tried using WMI Win32_SoftwareElement and also enumerating the following registry keys

  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
  • HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall
  • HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall
  • HKCU\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall

What I've found out is that although it's the fastest method (as opposed to WMI) it's not as reliable. Not all apps are listed, and out of those listed, not all have their InstallLocation properly set. Searching Win32_SoftwareElement yielded the best result, but it is sluggish, wondering if there is any better alternative, perhaps a reg location I failed to include in my search, or if there are other alternatives to WMI, like P/Invoke.

Was it helpful?

Solution

The install location isn't always set because it's not automatic. Some installs might not be MSI-based installs and I don't know if the other tools that are used will all set the location. If it's an MSI-based install the location is there only if the package developer sets the ARPINSTALLOCATION property during the install to the actual location.

http://msdn.microsoft.com/en-us/library/aa367589(v=vs.85).aspx

The plain old Win32 Windows Installer API to enumerate installed products is MsiEnumProducts () to return the ProductCode values one at a time, then to call MsiGetProductInfo() passing that product code guid and asking for INSTALLPROPERTY_INSTALLLOCATION, but again that's only there if the MSI developer did the right thing.

There's no reliable way to get installed products all at once because some are MSI-based, some are not, and they may or may not set the install location, and that install location is typically only the application directory and doesn't include files installed in any number of other places so it's value is minimal. Trawling through the registry and making decisions on what you find seems to be the only way to get the list.

Having said all that, short answer is that if you know a ProductCode then call MsiGetProductInfo() as above and get the location!

OTHER TIPS

It looks like there's no clear answer on StackOverflow how to do this, even though it's possible to do it somewhat reliably in case you know that installer is using MSI. I'll use C++, but it's simple to port it to C#:

  • First, we must obtain the ProductId. You either know it already, use gwmi win32_product -filter "Name like '%<product_name_pattern>%'" -namespace root/cimv2, or some other way to obtain it (e.g. with MsiEnumProductsW and MsiGetProductInfoW or by using upgrade_code and MsiEnumRelatedProductsW)
  • we try getting the InstallLocation directly:
  DWORD buf_size = MAX_PATH;
  wchar_t buf[MAX_PATH];
  if(ERROR_SUCCESS != MsiGetProductInfoW(Product_ID, INSTALLPROPERTY_INSTALLLOCATION, buf, &buf_size) || !buf_size)
  {
    return buf;
  }

As you already know, that won't work for some installers, because setting ARPINSTALLLOCATION property is optional (interesting design choice). However, Windows still correctly uninstalls those, so it must know something that we don't. MSI uses simple relational DB to organize its stuff, and Orca allows us to browse it. For example, here's node.js installer:

enter image description here Component column is just a name node authors used/generated via some installer toolset, KeyPath is a file name (or registry key etc.) which this component installs, and finally ComponentId is a GUID and used by Windows to uniquely identify files to uninstall.

Since we have the ProductId, we can find path of the cached .msi installer and query its DB to get all ComponentIds and their actual paths using MsiGetComponentPathW:

using unique_msi_handle = wil::unique_any<MSIHANDLE, decltype(&::MsiCloseHandle), ::MsiCloseHandle>;

if(ERROR_SUCCESS != MsiGetProductInfoW(Product_ID, INSTALLPROPERTY_LOCALPACKAGE, nullptr, &package_path_size))
{
  return;
}
std::wstring package_path(++package_path_size, L'\0');

if(ERROR_SUCCESS != MsiGetProductInfoW(Product_ID, INSTALLPROPERTY_LOCALPACKAGE, package_path.data(), &package_path_size))
{
  return;
}
package_path.resize(size(package_path) - 1); // trim additional \0 which we got from MsiGetProductInfoW

unique_msi_handle db_handle;
if(ERROR_SUCCESS != MsiOpenDatabaseW(package_path.data(), (wchar_t *)MSIDBOPEN_READONLY, &db_handle))
{
  return;
}

unique_msi_handle view;
if(ERROR_SUCCESS != MsiDatabaseOpenViewW(db_handle.get(), L"select ComponentId from Component", &view))
{
  return;
}

if(ERROR_SUCCESS != MsiViewExecute(view.get(), 0))
{
  return;
}

unique_msi_handle record;
while(ERROR_SUCCESS == MsiViewFetch(view.get(), &record))
{
  wchar_t component_id[guid_length];
  DWORD component_id_len = guid_length;
  MsiRecordGetStringW(record.get(), 1, component_id, &component_id_len);
  wchar_t path[256];
  DWORD path_size = 256;
  MsiGetComponentPathW(Product_ID, component_id, path, &path_size);

  std::wcout << component_id << L": " << path << '\n';
}

As @PhilDW mentioned, this probably crawls registry under the hood. Output for node.js:

{BE71D092-38E4-5DED-B176-D60079E51615}: C:\Program Files\nodejs\node.exe
{1A357DF6-3AF1-5A76-AE55-3676CCFA4513}: 22:\SOFTWARE\Node.js\InstallPath
{26837A22-55BA-5207-B2DE-0F7366E8FB8E}: C:\Program Files\nodejs\nodevars.bat
{EE1FC8BD-57FF-514B-8950-359974C2C6B9}: C:\Program Files\nodejs\install_tools.bat
{8B344AC8-9B54-5327-9D16-CE528884AC7E}: C:\Program Files\nodejs\node_etw_provider.man
{A194E0CC-E739-5C8B-947E-BD9463D8341A}: 21:\SOFTWARE\Node.js\Components\NodeStartMenuShortcuts
{B466EFC0-4255-51C5-8E26-3D8258F1AB57}: C:\Program Files\nodejs\npm.cmd
{CA3A5DB4-0430-533D-83BC-7044D32BAE9E}: C:\Program Files\nodejs\npm
{1BDBC761-41B0-51F4-8D42-42AD24077B91}: C:\Program Files\nodejs\npx.cmd
{F4384E08-1315-5F41-B086-F78BD6C0EE84}: C:\Program Files\nodejs\npx
{781AE8C4-C809-5E2B-A2C8-132EE6210483}: C:\Program Files\nodejs\node_modules\npm\npmrc
...

As you can see, there's a lot of options to go from here:

  • Inferring the installation path using the all components (if you don't know anything about the software you uninstalling).
  • If you know some file which is placed in the installation root, e.g. node.exe, just find take its prefix.

Hope this helps.

Adding to the Accepted answer, where the target program does not use the InstallLocation property to tell us where it was installed. You can get the program install location by enumerating the program installed components, the solution will also include program files installed in other locations. The solution is very fast compared to WMI.

  1. Assuming we have the target program code, use the MsiGetProductInfo Windows Installer API to get the program cached package path INSTALLPROPERTY_LOCALPACKAGE .

     <DllImport("msi.dll", CharSet:=CharSet.Unicode)>
     Private Function MsiGetProductInfo(ByVal product As String, ByVal [property] As String, <Out> ByVal valueBuf As StringBuilder, ByRef len As Int32) As Int32
     End Function
    
     Dim len = 512
     Dim sb As New StringBuilder(len)
     MsiGetProductInfo("{643C3762-7237-49F5-AD5E-5309C1B8EBCD}", "LocalPackage", sb, len)
    
     Dim msiPath = sb.ToString
    
  2. Parse the program Windows Installer database by installing this package: https://www.nuget.org/packages/DTF-Unofficial/ which is a wrapper of Windows Installer APIs.

  3. Query the program component table to enumerate the program component Ids.

     Dim lstComponents As New List(Of String)
    
     Using database = New Database(msiPath, DatabaseOpenMode.[ReadOnly])
    
         Using view = database.OpenView(database.Tables("Component").SqlSelectString)
    
             view.Execute()
    
             For Each rec In view
    
                 Using rec
                     Console.WriteLine("{0} = {1}", rec.GetString("ComponentId"), rec.GetString("Component"))
                     lstComponents.Add(rec.GetString("ComponentId"))
                 End Using
    
             Next
    
         End Using
    
     End Using
    
  4. Get each program component file path/Registry location using the MsiGetComponentPath API

     <DllImport("msi.dll", CharSet:=CharSet.Unicode)>
     Public Function MsiGetComponentPath(ByVal szProduct As String, ByVal szComponent As String, <Out> ByVal lpPathBuf As StringBuilder, ByRef pcchBuf As UInt32) As UInt32
     End Function
    
     For Each comId In lstComponents
    
         Dim sbPath As StringBuilder = New StringBuilder(500)
    
         Dim chs As UInteger = 500
         MsiGetComponentPath("{643C3762-7237-49F5-AD5E-5309C1B8EBCD}", comId, sbPath, chs)
    
         Console.WriteLine(sbPath.ToString)
    
     Next
    

References:

MsiGetProductInfo

MsiGetComponentPath

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top