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:
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.