Overview
The CDetectDotNet
class detects whether the .NET Framework is
installed on a machine and also retrieves the list of Framework versions
currently installed. Not unpredictably, it's an unmanaged C++ class with zero
dependency on anything .NET related - it would have been utterly worthless if it
required the .NET framework to run. I believe the class would be pretty useful in installer
applications, that might need to detect whether the target machine has .NET
installed and if so, whether a specific version is available. One important
thing to keep in mind is that the .NET versions retrieved represent CLR versions
(along with the matching BCL), and may not necessarily mean that the
corresponding SDKs are present; the distinction to be observed here is that
between the full SDK and the .NET runtime.
Using the class
Add DetectDotNet.h and DetectDotNet.cpp to your project.
#include "DetectDotNet.h"
CDetectDotNet detect;
vector<string> CLRVersions;
cout << "Is .NET present : "
<< (detect.IsDotNetPresent() ? "Yes" : "No") << endl;
TCHAR szPath[300];
cout << "Root Path : "
<< (detect.GetInstallRootPath(szPath, 299) ? szPath : "") << endl;
cout << "Number of CLRs detected : "
<< (int)detect.EnumerateCLRVersions(CLRVersions) << endl;
cout << "CLR versions available :-" << endl;
for(vector<string>::iterator it = CLRVersions.begin();
it < CLRVersions.end(); it++)
{
cout << *it << endl;
}
cout << "Press any key..." << endl;
getch();
Sample Output
Is .NET present : Yes
Root Path : C:\WINDOWS\Microsoft.NET\Framework\
Number of CLRs detected : 2
CLR versions available :-
2.0.50215
1.1.4322
Press any key...
Public Interface
CDetectDotNet();
Constructs a CDetectDotNet
object.
bool IsDotNetPresent();
Detects if .NET is present on a system.
Returns true
if .NET is detected on the system and false
otherwise.
bool GetInstallRootPath(TCHAR* szRootPath, DWORD dwBufferSize);
Retrieves the root installation path of the .NET Framework
szRootPath
- The .NET root installation path is returned in
szRootPath
.
dwBufferSize
- Specifies the length of szRootPath
.
Returns true
if successful and false
otherwise.
size_t EnumerateCLRVersions(vector<string>& CLRVersions);
Enumerates the list of active CLR versions in the system.
CLRVersions
- This will contain the list of CLR versions
detected on the system.
Returns the count of CLR versions detected.
Implementation
Detecting the .NET Framework
The extremely simple technique I use to detect whether the .NET Framework is
present on the system is to LoadLibrary
"mscoree.dll" and if that
succeeds, I also do a GetProcAddress
for "GetCORVersion"
which takes care of scenarios where the OS version comes with a placeholder DLL.
I also cache the result (in the class constructor), so that subsequent calls
need not repeat the LoadLibrary
/GetProcAddress
calls.
bool CDetectDotNet::IsDotNetPresent()
{
return m_bDotNetPresent;
}
bool CDetectDotNet::IsDotNetPresentInternal()
{
bool bRet = false;
HMODULE hModule = LoadLibrary(_T("mscoree"));
if(hModule)
{
bRet = (GetProcAddress(hModule, "GetCORVersion") != NULL);
FreeLibrary(hModule);
}
return bRet;
}
Enumerating CLR versions
Here's the mechanism I use to enumerate the available CLR versions on a system.
- Get the root install path for the .NET Framework (obtained from the
registry). Typically, this would be something like C:\WINDOWS\Microsoft.NET\Framework.
- Find all directories in the root install path. Most (but not all) of
these directories would be the base paths of individual .NET Framework
versions.
- Use
GetRequestedRuntimeInfo
to query the presence of the CLR
versions detected in the previous step. Surprisingly, the pwszVersion
parameter that GetRequestedRuntimeInfo
takes is actually the
directory name within the .NET root path where a specific CLR version
exists. Thus, if you rename the default folder name (usually something like
"v2.0.50215") to something else, GetRequestedRuntimeInfo
will
still succeed.
- Once
GetRequestedRuntimeInfo
has succeeded, we still need
to get the version string (to take care of situations where someone has
renamed the folder name). To do this I simply look for mscorlib.dll
within that folder and extract its file version, and you can be sure that
any successful implementation of the framework will definitely have an
mscorlib.dll in its installation folder.
Note - You'll find GetRequestedRuntimeInfo
in
mscoree.h. Here's what I have in the version that comes with VS.NET 2005
Beta 2.
STDAPI GetRequestedRuntimeInfo(
LPCWSTR pExe, LPCWSTR pwszVersion, LPCWSTR pConfigurationFile,
DWORD startupFlags, DWORD runtimeInfoFlags,
LPWSTR pDirectory, DWORD dwDirectory, DWORD *dwDirectoryLength,
LPWSTR pVersion, DWORD cchBuffer, DWORD* dwlength);
Here's a screenshot of my .NET installation root folder.
Notice the folder named "renamed", which is actually the installation folder
for "v2.0.50215". The call to GetRequestedRuntimeInfo
with
pwszVersion
set to "renamed" actually succeeds (possibly because GetRequestedRuntimeInfo
uses a very similar technique to what I do to extract the version). Funnily, the
version string returned by the function in pVersion
is the name of
the folder and not the real version string, but this is not a big bother as we
know that there will be an mscorlib.dll in this folder and that its
version string will give us the CLR version contained in the folder.
Getting the root installation folder
This is extracted from the registry.
HKEY_LOCAL_MACHINE
SOFTWARE
Microsoft
.NETFramework : InstallRoot (REG_SZ)
bool CDetectDotNet::GetInstallRootPath(TCHAR* szRootPath, DWORD dwBufferSize)
{
bool bRet = false;
if(m_szInstallRootPath)
{
size_t siz = _tcslen(m_szInstallRootPath);
if(dwBufferSize > siz)
{
_tcsncpy(szRootPath, m_szInstallRootPath, siz);
szRootPath[siz] = NULL;
bRet = true;
}
}
return bRet;
}
bool CDetectDotNet::GetInstallRootPathInternal(TCHAR* szRootPath, DWORD dwBufferSize)
{
bool bRet = false;
TCHAR szRegPath[] = _T("SOFTWARE\\Microsoft\\.NETFramework");
HKEY hKey = NULL;
if(RegOpenKeyEx(HKEY_LOCAL_MACHINE, szRegPath, 0,
KEY_READ, &hKey) == ERROR_SUCCESS)
{
DWORD dwSize = dwBufferSize;
if(RegQueryValueEx(hKey, _T("InstallRoot"), NULL, NULL,
reinterpret_cast<LPBYTE>(szRootPath),
&dwSize) == ERROR_SUCCESS)
{
bRet = (dwSize <= dwBufferSize);
}
RegCloseKey(hKey);
}
return bRet;
}
Getting the .NET version from the mscorlib assembly
string CDetectDotNet::GetVersionFromFolderName(string szFolderName)
{
string strRet = "<Version could not be extracted from mscorlib>";
TCHAR szRootPath[g_cPathSize];
if(GetInstallRootPath(szRootPath, g_cPathSize))
{
USES_CONVERSION;
string szFilepath = T2A(szRootPath);
szFilepath += (szFolderName + "\\mscorlib.dll");
string s = GetDotNetVersion(A2CT(szFilepath.c_str()));
if(s.size() > 0)
strRet = s;
}
return strRet;
}
string CDetectDotNet::GetDotNetVersion(LPCTSTR szFolder)
{
string strRet = _T("");
LPVOID m_lpData = NULL;
TCHAR buff[MAX_PATH + 1] = {0};
_tcsncpy(buff, szFolder, MAX_PATH);
DWORD dwHandle = 0;
DWORD dwVerInfoSize = GetFileVersionInfoSize(buff, &dwHandle);
if(dwVerInfoSize != 0)
{
m_lpData = malloc(dwVerInfoSize);
if(GetFileVersionInfo(buff, dwHandle,
dwVerInfoSize, m_lpData) == FALSE)
{
free(m_lpData);
m_lpData = NULL;
}
else
{
UINT cbTranslate = 0;
struct LANGANDCODEPAGE
{
WORD wLanguage;
WORD wCodePage;
} *lpTranslate;
if(VerQueryValue(m_lpData,_T("\\VarFileInfo\\Translation"),
(LPVOID*)&lpTranslate,&cbTranslate))
{
int count = (int)(cbTranslate/sizeof(struct LANGANDCODEPAGE));
for(int i=0; i < count; i++ )
{
TCHAR SubBlock[128];
HRESULT hr = StringCchPrintf(SubBlock,
127,_T("\\StringFileInfo\\%04x%04x\\%s"),
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage,_T("FileVersion"));
if(SUCCEEDED(hr))
{
UINT dwBytes = 0;
TCHAR* lpBuffer;
if(VerQueryValue(m_lpData, SubBlock,
(LPVOID*)&lpBuffer, &dwBytes))
{
USES_CONVERSION;
strRet = T2A(lpBuffer);
for(unsigned int x = 0, j = 0; j < strRet.size(); j++)
{
if(strRet[j] == '.')
{
if(++x == 3)
{
strRet.erase(j,strRet.size() - j);
break;
}
}
}
break;
}
}
}
}
}
}
return strRet;
}
Check if a specific .NET version exists
bool CDetectDotNet::CheckForSpecificCLRVersionInternal(LPCWSTR pszVersion)
{
bool bRet = false;
if( m_bDotNetPresent )
{
UINT prevErrMode = SetErrorMode(SEM_FAILCRITICALERRORS);
HMODULE hModule = LoadLibrary(_T("mscoree"));
if(hModule)
{
FPGetRequestedRuntimeInfo pGetRequestedRuntimeInfo =
reinterpret_cast<FPGetRequestedRuntimeInfo>(
GetProcAddress(hModule, "GetRequestedRuntimeInfo"));
if(pGetRequestedRuntimeInfo)
{
LPWSTR dirBuff = NULL;
DWORD dwDir = 0;
LPWSTR verBuff = NULL;
DWORD dwVer = 0;
pGetRequestedRuntimeInfo(NULL, pszVersion,
NULL,0,0,
dirBuff, dwDir, &dwDir,
verBuff, dwVer, &dwVer);
dirBuff = new WCHAR[dwDir + 1];
verBuff = new WCHAR[dwVer + 1];
HRESULT hr = pGetRequestedRuntimeInfo(NULL, pszVersion,
NULL,0,0,dirBuff, dwDir, &dwDir,verBuff, dwVer, &dwVer);
bRet = (hr == S_OK);
delete[] verBuff;
delete[] dirBuff;
}
FreeLibrary(hModule);
}
SetErrorMode(prevErrMode);
}
return bRet;
}
Get list of CLR versions
size_t CDetectDotNet::EnumerateCLRVersions(vector<string>& CLRVersions)
{
CLRVersions.clear();
USES_CONVERSION;
vector<string> PossibleCLRVersions;
EnumeratePossibleCLRVersionsInternal(PossibleCLRVersions);
for(vector<string>::iterator it = PossibleCLRVersions.begin();
it < PossibleCLRVersions.end(); it++)
{
if(CheckForSpecificCLRVersionInternal(A2CW((*it).c_str())))
{
CLRVersions.push_back(GetVersionFromFolderName(*it));
}
}
return CLRVersions.size();
}
size_t CDetectDotNet::EnumeratePossibleCLRVersionsInternal(
vector<string>& PossibleCLRVersions)
{
PossibleCLRVersions.clear();
if(m_bDotNetPresent)
{
TCHAR szRootBuff[g_cPathSize];
if(GetInstallRootPath(szRootBuff, g_cPathSize))
{
WIN32_FIND_DATA finddata = {0};
_tcsncat(szRootBuff, _T("*"), 1);
HANDLE hFind = FindFirstFile(szRootBuff, &finddata);
if(hFind != INVALID_HANDLE_VALUE)
{
do
{
if( finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY )
{
PossibleCLRVersions.push_back(finddata.cFileName);
}
}while(FindNextFile(hFind, &finddata));
FindClose(hFind);
}
}
}
return PossibleCLRVersions.size();
}
Final notes
I haven't tested this code out on every Windows or .NET version out there, so
I cannot guarantee that it will work correctly all the time. But if it does not
work for you, please let me know of it through the forum, and if you post an
accurate problem description, I'll make efforts to implement a fix or a
work-around. But you need to tell me, because if I don't know that a bug exists,
I am not going to be able to fix it. Enjoy!
History
- July 21st 2005 - v2.2
- Fixed a bug in the constructor that had crept in during the 2.1 update.
(I was incorrectly calling
IsDotNetPresent
to initialize m_bDotNetPresent
when I
should have been calling IsDotNetPresentInternal
)
- July 17th 2005 - v2.1
- Caches
IsDotNetPresent
and GetInstallRootPath
internally (so the actual detection code is executed only once - in the constructor)
- Modified the version enumeration code so that the class can detect CLR
versions even if their directory names have been modified. It does this by
extracting the version string out of the mscorlib.dll for that
specific CLR version.
- July 16th 2005 - v2.0
- Removed the dependencies on mscoree.h and mscoree.lib.
- Removed the need to invoke an external detector stub executable.
- Uses
GetRequestedRuntimeInfo
to check for a
specific CLR version.
- Updated
IsDotNetPresent
to verify that the
mscoree.dll that LoadLibrary
successfully loaded is not a placeholder
DLL shipped in earlier OS versions.
- July 15th 2005 - v1.0
- First version of the class uploaded.