Finding applications installed on a machine (the ones that you see in Control Panel Add/Remove programs) could be a little bit tricky, because there isn’t a bulletproof API or method. Each of the available methods has its own weak points. WMI is slow and can actually be disabled on a machine. MSI API only shows applications installed with an MSI, and reading directly from the Windows Registry is not an officially supported alternative. Thus it is an open point which one is the most appropriate, though the official answer will probably be MSI API.

In this post I will go through all of these three methods and show how to query for the installed applications and display the name, publisher, vendor and installation location (if available). Notice these are just some samples, and if you want to use this in your applications you’ll probably want to do additional things like better error checking. Because I want the code to work both with ANSI and UNICODE I will use the following defines

#include 
#include 

#ifdef _UNICODE
#define tcout       wcout
#define tstring     wstring
#else
#define tcout       cout
#define tstring     string
#endif

WMI
Win32_Product is a WMI class that represents a product installed by Windows Installer. For fetching the list of installed applications with WMI I will reuse the WMIQuery class I first shown in this post. You need to include Wbemidl.h and link with wbemuuid.lib.

In the code shown below WmiQueryValue() is a function that reads a property from the current record and returns it as an STL string (UNICODE or ANSI). WmiEnum() is a function that fetches and displays in the console all the installed applications.

class WMIQuery
{
   IWbemLocator* m_pLocator;
   IWbemServices* m_pServices;

public:
   WMIQuery():
      m_pLocator(NULL),
      m_pServices(NULL)
   {
   }

   bool Initialize()
   {
      // Obtain the initial locator to WMI
      HRESULT hr = ::CoCreateInstance(
         CLSID_WbemLocator,
         0,
         CLSCTX_INPROC_SERVER,
         IID_IWbemLocator, (LPVOID *) &m_pLocator);

      if (FAILED(hr))
      {
         cerr << "Failed to create IWbemLocator object. Err code = 0x" << hex << hr << endl;
         return false;
      }

      // Connect to WMI through the IWbemLocator::ConnectServer method
      // Connect to the root\cimv2 namespace with the current user
      hr = m_pLocator->ConnectServer(
         _bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace
         NULL,                    // User name. NULL = current user
         NULL,                    // User password. NULL = current
         0,                       // Locale. NULL indicates current
         NULL,                    // Security flags.
         0,                       // Authority (e.g. Kerberos)
         0,                       // Context object
         &m_pServices             // pointer to IWbemServices proxy
         );

      if (FAILED(hr))
      {
         cerr << "Could not connect. Error code = 0x" << hex << hr << endl;
         m_pLocator->Release();
         m_pLocator = NULL;
         return false;
      }

      // Set security levels on the proxy
      hr = ::CoSetProxyBlanket(
         m_pServices,                 // Indicates the proxy to set
         RPC_C_AUTHN_WINNT,           // RPC_C_AUTHN_xxx
         RPC_C_AUTHZ_NONE,            // RPC_C_AUTHZ_xxx
         NULL,                        // Server principal name
         RPC_C_AUTHN_LEVEL_CALL,      // RPC_C_AUTHN_LEVEL_xxx
         RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx
         NULL,                        // client identity
         EOAC_NONE                    // proxy capabilities
         );

      if (FAILED(hr))
      {
         cerr << "Could not set proxy blanket. Error code = 0x" << hex << hr << endl;
         m_pServices->Release();
         m_pServices = NULL;
         m_pLocator->Release();
         m_pLocator = NULL;
         return false;
      }

      return true;
   }

   IEnumWbemClassObject* Query(LPCTSTR strquery)
   {
      IEnumWbemClassObject* pEnumerator = NULL;
      HRESULT hr = m_pServices->ExecQuery(
         bstr_t("WQL"),
         bstr_t(strquery),
         WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
         NULL,
         &pEnumerator);

      if (FAILED(hr))
      {
         cerr << "Query for operating system name failed. Error code = 0x" << hex << hr < endl;
         return NULL;
      }

      return pEnumerator;
   }

   ~WMIQuery()
   {
      if(m_pServices != NULL)
      {
         m_pServices->Release();
         m_pServices = NULL;
      }

      if(m_pLocator != NULL)
      {
         m_pLocator->Release();
         m_pLocator = NULL;
      }
   }
};

tstring WmiQueryValue(IWbemClassObject* pclsObj,
                      LPCWSTR szName)
{
    tstring value;

    if(pclsObj != NULL && szName != NULL)
    {
        VARIANT vtProp;

        HRESULT hr = pclsObj->Get(szName, 0, &vtProp, 0, 0);
        if(SUCCEEDED(hr))
        {
            if(vtProp.vt == VT_BSTR && ::SysStringLen(vtProp.bstrVal) > 0)
            {
#ifdef _UNICODE
                value = vtProp.bstrVal;
#else
                int len = ::SysStringLen(vtProp.bstrVal)+1;
                if(len > 0)
                {
                    value.resize(len);
                    ::WideCharToMultiByte(CP_ACP,
                                          0,
                                          vtProp.bstrVal,
                                          -1,
                                          &value[0],
                                          len,
                                          NULL,
                                          NULL);
                }
#endif
            }
        }
    }

    return value;
}

void WmiEnum()
{
    HRESULT hres;

    // Initialize COM.
    hres =  ::CoInitializeEx(0, COINIT_MULTITHREADED);
    if (FAILED(hres))
    {
        cout << "Failed to initialize COM library. Error code = 0x" << hex << hres << endl;
        return;
    }

    // Set general COM security levels
    hres =  ::CoInitializeSecurity(
        NULL,
        -1,                          // COM authentication
        NULL,                        // Authentication services
        NULL,                        // Reserved
        RPC_C_AUTHN_LEVEL_DEFAULT,   // Default authentication
        RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation
        NULL,                        // Authentication info
        EOAC_NONE,                   // Additional capabilities
        NULL                         // Reserved
        );

    if (FAILED(hres))
    {
        cout << "Failed to initialize security. Error code = 0x" << hex << hres << endl;
        ::CoUninitialize();
        return;
    }
    else
    {
        WMIQuery query;
        if(query.Initialize())
        {
            IEnumWbemClassObject* pEnumerator = query.Query(_T("SELECT * FROM Win32_Product"));

            if(pEnumerator != NULL)
            {
                // Get the data from the query
                IWbemClassObject *pclsObj;
                ULONG uReturn = 0;

                while (pEnumerator)
                {
                    HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn);

                    if(0 == uReturn)
                    {
                        break;
                    }

                    // find the values of the properties we are interested in
                    tstring name = WmiQueryValue(pclsObj, L"Name");
                    tstring publisher = WmiQueryValue(pclsObj, L"Vendor");
                    tstring version = WmiQueryValue(pclsObj, L"Version");
                    tstring location = WmiQueryValue(pclsObj, L"InstallLocation");

                    if(!name.empty())
                    {
                        tcout << name << endl;
                        tcout << "  - " << publisher << endl;
                        tcout << "  - " << version << endl;
                        tcout << "  - " << location << endl;
                        tcout << endl;
                    }

                    pclsObj->Release();
                }

                pEnumerator->Release();
            }
        }
    }

    // unintializa COM
    ::CoUninitialize();
}

A sample from the output of this WmiEnum() function looks like this:

Java(TM) 6 Update 25
– Oracle
– 6.0.250
– C:\Program Files\Java\jre6\

Java(TM) SE Development Kit 6 Update 25
– Oracle
– 1.6.0.250
– C:\Program Files\Java\jdk1.6.0_25\

Microsoft .NET Framework 4 Client Profile
– Microsoft Corporation
– 4.0.30319
-

Microsoft Sync Framework Services v1.0 SP1 (x86)
– Microsoft Corporation
– 1.0.3010.0
-

Microsoft ASP.NET MVC 2 – Visual Studio 2010 Tools
– Microsoft Corporation
– 2.0.50217.0
-

Adobe Reader X (10.0.1)
– Adobe Systems Incorporated
– 10.0.1
– C:\Program Files\Adobe\Reader 10.0\Reader\

One can notice that the code is relatively long, but most important it is very slow.

MSI API
Two of the MSI API functions can help fetching the list of installed applications:

  • MsiUnumProductsEx: enumerates through one or all the instances of products that are currently advertised or installed (requires Windows Installer 3.0 or newer)
  • MsiGetProductInfoEx: returns product information for advertised and installed products

In order to use these functions you need to include msi.h and link to msi.lib.

In the code below, MsiQueryProperty() is a function that returns the value of product property (as a tstring as defined above) by calling MsiGetProductInfoEx. MsiEnum() is a function that iterates through all the installed applications and prints in the console the name, publisher, version and installation location.

tstring MsiQueryProperty(LPCTSTR szProductCode,
                         LPCTSTR szUserSid,
                         MSIINSTALLCONTEXT dwContext,
                         LPCTSTR szProperty)
{
    tstring value;

    DWORD cchValue = 0;
    UINT ret2 = ::MsiGetProductInfoEx(
        szProductCode,
        szUserSid,
        dwContext,
        szProperty,
        NULL,
        &cchValue);

    if(ret2 == ERROR_SUCCESS)
    {
        cchValue++;
        value.resize(cchValue);

        ret2 = ::MsiGetProductInfoEx(
            szProductCode,
            szUserSid,
            dwContext,
            szProperty,
            (LPTSTR)&value[0],
            &cchValue);
    }

    return value;
}

void MsiEnum()
{
    UINT ret = 0;
    DWORD dwIndex = 0;
    TCHAR szInstalledProductCode[39] = {0};
    TCHAR szSid[128] = {0};
    DWORD cchSid;
    MSIINSTALLCONTEXT dwInstalledContext;

    do
    {
        memset(szInstalledProductCode, 0, sizeof(szInstalledProductCode));
        cchSid = sizeof(szSid)/sizeof(szSid[0]);

        ret = ::MsiEnumProductsEx(
            NULL,           // all the products in the context
            _T("s-1-1-0"),  // i.e.Everyone, all users in the system
            MSIINSTALLCONTEXT_USERMANAGED | MSIINSTALLCONTEXT_USERUNMANAGED | MSIINSTALLCONTEXT_MACHINE,
            dwIndex,
            szInstalledProductCode,
            &dwInstalledContext,
            szSid,
            &cchSid);

        if(ret == ERROR_SUCCESS)
        {
            tstring name = MsiQueryProperty(
                szInstalledProductCode,
                cchSid == 0 ? NULL : szSid,
                dwInstalledContext,
                INSTALLPROPERTY_INSTALLEDPRODUCTNAME);

            tstring publisher = MsiQueryProperty(
                szInstalledProductCode,
                cchSid == 0 ? NULL : szSid,
                dwInstalledContext,
                INSTALLPROPERTY_PUBLISHER);                

            tstring version = MsiQueryProperty(
                szInstalledProductCode,
                cchSid == 0 ? NULL : szSid,
                dwInstalledContext,
                INSTALLPROPERTY_VERSIONSTRING);

            tstring location = MsiQueryProperty(
                szInstalledProductCode,
                cchSid == 0 ? NULL : szSid,
                dwInstalledContext,
                INSTALLPROPERTY_INSTALLLOCATION);            

            tcout << name << endl;
            tcout << "  - " << publisher << endl;
            tcout << "  - " << version << endl;
            tcout << "  - " << location << endl;
            tcout << endl;

            dwIndex++;
        }

    } while(ret == ERROR_SUCCESS);
}

And this is a sample for the WmiEnum() function.

Java(TM) 6 Update 25
– Oracle
– 6.0.250
– C:\Program Files\Java\jre6\

Java(TM) SE Development Kit 6 Update 25
– Oracle
– 1.6.0.250
– C:\Program Files\Java\jdk1.6.0_25\

Microsoft .NET Framework 4 Client Profile
– Microsoft Corporation
– 4.0.30319
-

Microsoft Sync Framework Services v1.0 SP1 (x86)
– Microsoft Corporation
– 1.0.3010.0
-

Microsoft ASP.NET MVC 2 – Visual Studio 2010 Tools
– Microsoft Corporation
– 2.0.50217.0
-

Adobe Reader X (10.0.1)
– Adobe Systems Incorporated
– 10.0.1
– C:\Program Files\Adobe\Reader 10.0\Reader\

Windows Registry
Installed applications are listed in Windows Registry under HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall. The KB247501 article explains the structure of the information under this Registry key. Make sure you read it if you decide to use this approach.

In the code shown below, RegistryQueryValue() is a function that queries the value of a name/value pair in the registry and returns the value as a tstring. RegistryEnum() is a function that prints to the console all the installed application as found in the registry.

tstring RegistryQueryValue(HKEY hKey,
                           LPCTSTR szName)
{
    tstring value;

    DWORD dwType;
    DWORD dwSize = 0;

    if (::RegQueryValueEx(
        hKey,                   // key handle
        szName,                 // item name
        NULL,                   // reserved
        &dwType,                // type of data stored
        NULL,                   // no data buffer
        &dwSize                 // required buffer size
        ) == ERROR_SUCCESS && dwSize > 0)
    {
        value.resize(dwSize);

        ::RegQueryValueEx(
            hKey,                   // key handle
            szName,                 // item name
            NULL,                   // reserved
            &dwType,                // type of data stored
            (LPBYTE)&value[0],      // data buffer
            &dwSize                 // available buffer size
            );
    }

    return value;
}

void RegistryEnum()
{
    HKEY hKey;
    LONG ret = ::RegOpenKeyEx(
        HKEY_LOCAL_MACHINE,     // local machine hive
        _T("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall"), // uninstall key
        0,                      // reserved
        KEY_READ,               // desired access
        &hKey                   // handle to the open key
        );

    if(ret != ERROR_SUCCESS)
        return;

    DWORD dwIndex = 0;
    DWORD cbName = 1024;
    TCHAR szSubKeyName[1024];

    while ((ret = ::RegEnumKeyEx(
        hKey,
        dwIndex,
        szSubKeyName,
        &cbName,
        NULL,
        NULL,
        NULL,
        NULL)) != ERROR_NO_MORE_ITEMS)
    {
        if (ret == ERROR_SUCCESS)
        {
            HKEY hItem;
            if (::RegOpenKeyEx(hKey, szSubKeyName, 0, KEY_READ, &hItem) != ERROR_SUCCESS)
                continue;

            tstring name = RegistryQueryValue(hItem, _T("DisplayName"));
            tstring publisher = RegistryQueryValue(hItem, _T("Publisher"));
            tstring version = RegistryQueryValue(hItem, _T("DisplayVersion"));
            tstring location = RegistryQueryValue(hItem, _T("InstallLocation"));

            if(!name.empty())
            {
                tcout << name << endl;
                tcout << "  - " << publisher << endl;
                tcout << "  - " << version << endl;
                tcout << "  - " << location << endl;
                tcout << endl;
            }

            ::RegCloseKey(hItem);
        }
        dwIndex++;
        cbName = 1024;
    }
    ::RegCloseKey(hKey);
}

And a sample output of the RegistryEnum() function:

Java(TM) SE Development Kit 6 Update 25

– Oracle
– 1.6.0.250
– C:\Program Files\Java\jdk1.6.0_25\

Microsoft Visual Studio 2005 Tools for Office Runtime

– Microsoft Corporation
– 8.0.60940.0
-

MSDN Library for Visual Studio 2008 – ENU

– Microsoft
– 9.0.21022
– C:\Program Files\MSDN\MSDN9.0\

Microsoft SQL Server Compact 3.5 SP2 ENU

– Microsoft Corporation
– 3.5.8080.0
– C:\Program Files\Microsoft SQL Server Compact Edition\

Microsoft .NET Framework 4 Client Profile

– Microsoft Corporation
– 4.0.30319
-

, , , , Hits for this post: 5498 .

If you are still using Visual Studio 2005 and need to develop WCF services you need the following:

The problem with the later is that Microsoft no longer supports it. Visual Studio 2008 is supposed to be used for developing such projects. The license for the CTP has expired on June 30th 2008. You can read more about that here. What that means is that you can no longer develop WCF applications in Visual Studio 2005 and should upgrade to Visual Studio 2008. However, as an exercise, I wanted to see if I could still install the WCF extensions in Visual Studio 2005 and have it work side by side with Visual Studio 2008.

First, I had to find the old CTP with the Visual Studio 2005 extensions. It can be downloaded from here. However, when I run it, I got the following error:

WCF Extensions for Visual Studio 2005 Setup Error

WCF Extensions for Visual Studio 2005 Setup Error

The curious thing was that .NET 3.0 was already installed on my machine. I soon realized I was having .NET 3.0 SP2, and the installer was looking for .NET 3.0. Of course, you cannot install 3.0 when a newer version (such as 3.0 SP2) is already installed. So the only option was to change the installer.

Orca is a database editor, allowing you to create and edit MSI files and merge modules. It is provided as a part of Windows Installer SDK. But you can also find stand alone downloads, such as this.

After installing Orca, you can open the MSI file. From Tools > Validate… one can run a validation on the installer. It shown the following errors:

ICE08 ERROR Component: WCFSvcConfigEditor_dll has a duplicate GUID: {714D044E-3136-457E-ADD7-AE3D0FEF021A}
ICE16 ERROR ProductName: ‘Visual Studio 2005 extensions for .NET Framework 3.0 (WCF & WPF), November 2006 CTP’ is greater than 63 characters in length. Current length: 83
ICE77 ERROR CA_CommitHelpTransactionNoRB.3643236F_FC70_11D3_A536_0090278A1BB8 is a in-script custom action. It must be sequenced in between the InstallInitialize action and the InstallFinalize action in the InstallExecuteSequence table

I had to do the following fixes:

  • replace the first occurrence of this GUID {714D044E-3136-457E-ADD7-AE3D0FEF021A} with another one
  • trim the name “Visual Studio 2005 extensions for .NET Framework 3.0 (WCF & WPF), November 2006 CTP” to “Visual Studio 2005 extensions for .NET Framework 3.0 SP2″
  • change the Sequence number of the CA_CommitHelpTransactionNoRB.3643236F_FC70_11D3_A536_0090278A1BB8 action in he InstallExecuteSequence table from 6601 to 6599 so that it is in between InstallInitialize (1500) and InstallFinalize (6600)

But then, came the biggest problem: making the installer work with .NET 3.5 SP2. It was looking in registry for the following key: SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{15095BF3-A3D7-4DDF-B193-3A496881E003}. The GUID {15095BF3-A3D7-4DDF-B193-3A496881E003} corresponds to .NET 3.0. So in order to make it work with .NET 3.0 SP2 one needs to replace the GUID with {A3051CD0-2F64-3813-A88D-B8DCCDE8F8C7} for the SearchForWinFXruntimeX86Install signature in the RegLocator table.

Once those changes are made all you have to do is save. However, the “Copy embedded steams during ‘Save As’” option should be checked from Tools > Options > Database, before doing the saving. Otherwise the embedded CAB is not copied and the installation won’t work.

Running the modified installer works successfully.

Setup start page

Setup start page

Setup finished

Setup finished

If you start Visual Studio 2005 you can create a new project from one of the WCF project templates.

WCF Project Templates

WCF Project Templates

You can download the altered MSI file from here: The Visual Studio 2005 extensions for.NET Framework 3.0 SP2 (WCF & WPF) (1167).

Note: I must say this again: this scenario is no longer supported. The license for this CTP for WCF extensions for Visual Studio 2005 has expired in 2008. You should upgrade to Visual Studio 2008 to develop WCF applications.

, , , , Hits for this post: 14317 .