Monday, February 26, 2007

Four Ways to Detect Vista

Pop quiz. How do you detect Windows version? Simple, call the GetVersionEx API.

And now for a bonus question. How do you detect Windows version without a doubt? Well, that's not so simple. Do you disagree? Read on.

Compatibility mode

For some time (I think this functionality appeared in Windows XP), you can execute applications under a compatibility layer. Right-click your exe, select Properties and click on the Compatibility tab. Click on the Run this program in compatibility mode for. Now you can select from the list of many operating system versions - from Windows 95 onwards.

When you do this, plenty of things happen. Firstly, Windows starts faking OS version to your program. Secondly, some API functions work more like they did in the selected OS version.

Why would you do this? Maybe the program in question is not well-behaved and needs this kind of help from the OS. Or maybe it just tests for supported OS version too aggressively and you want to circumvent this test.

In all cases, result is the same - what you get from GetVersionEx is not the true answer. So, I have to repeat my bonus question - how could you detect the true OS version?

Four ways to detect OS

I run into that problem few weeks ago. We have an application that is not yet Vista-ready, mostly because of some DirectX incompatibilities. We don't want negative user experience and therefore we don't allow the app to run on Vista at all. Still, some users may be smart enough to enable compatibility mode for this application, which would make OS version test useless (sadly, compatibility mode doesn't help this application to work correctly on Vista).

I found no good solution but luckily my fellow Slovenian Delphi users did. Even more, they found three alternative solutions.

One is to check for system file that was not present in previous OS versions. For example, one can test for presence of %WINDIR%\System32\ndfetw.dll.

One is to check Notepad.exe version. Under Vista, Notepad has version 6.something while it was 5.something in XP.

And the last one (and the best, in my opinion) is to check if specific API is exported. For example, Vista exports GetLocaleInfoEx from kernel32.dll while previous Windowses didn't.

That's it - now you have one way to get simulated OS version (actually, there are two - you can check the HLKM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentVersion key) and three to get true OS version.

The last approach is so flexible that we (Miha-R from the Delphi-SI forum, who did most of the work, and me) turned it into a function that can detect all 32-bit OS versions simply by checking for exported API functions. After some work, we created a list of OS-version-determining APIs, all exported from kernel32.dll.

  Vista Server 2003 XP SP1 XP 2000 ME NT 4 98 95 OSR 2 NT 3 95
GetLocaleInfoEx  x                    
GetLargePageMinimum  x  x                  
GetDLLDirectory  x  x  x                
GetNativeSystemInfo  x  x  x  x              
ReplaceFile  x  x  x  x  x            
OpenThread  x  x  x  x  x  x          
GetThreadPriorityBoost  x  x  x  x  x    x        
IsDebuggerPresent  x  x  x  x  x  x  x  x      
GetDiskFreeSpaceEx  x  x  x  x  x  x  x  x  x    
ConnectNamedPipe  x  x  x  x  x          x  
Beep  x  x  x  x  x  x    x    x  x

Writing the function - DSiGetTrueWindowsVersion - was quite simple; just proceed from newest OS to oldest and check for exported APIs. You can get this function (and many more) by downloading the freeware DSiWin32 unit (another effort of Slovenian Delphi community).

Checking for compatibility layer

It would be also interesting to detect whether the application is running under the compatibility layer. There is a registry key that contains this info - Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers, key name is full path of the executable - but the information itself is not reliable.

First, you have to check both HKLM and HKCU branches, as the compatibility may be set by current user and enforced by the system administrator. Second, on 64-bit Windows, you must check the (true) HKLM64 branch, not the (32-bit compatibility) HKLM branch (see the code below for more detail). And third, all this may be to no avail.

The problem lies in a fact that child processes inherit compatibility layer from parents. If you set compatibility mode for executable A and then run executable B from it, executable B won't have any compatibility data set in the registry but nevertheless it will run under the compatibility layer. Still, information in those keys may be of some use sometime.

Demo

Try running it as a normal app and under the compatibility layer.

program IsVista;

{$APPTYPE CONSOLE}

uses
Windows,
SysUtils,
GpVersion,
DSiWin32;

var
testFile: string;

begin
Writeln('GetVersionEx API: ', CDSiWindowsVersionStr[DSiGetWindowsVersion]);
Writeln('HKLM CurrentVersion registry value: ', DSiReadRegistry(
'\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'CurrentVersion', '',
HKEY_LOCAL_MACHINE));
Writeln('HKLM ProductName registry value: ', DSiReadRegistry(
'\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'ProductName', '',
HKEY_LOCAL_MACHINE));
testFile := IncludeTrailingPathDelimiter(DSiGetSystemFolder) + 'ndfetw.dll';
Writeln('Presence of file ', testFile, ': ', BoolToStr(FileExists(testFile), true));
Writeln('Notepad.exe version: ',
CreateResourceVersionInfo(IncludeTrailingPathDelimiter(DSiGetWindowsFolder) +
'notepad.exe').GetFormattedVersion(verFullDotted));
Writeln('Presence of GetLocaleInfoEx API: ',
BoolToStr(GetProcAddress(GetModuleHandle('kernel32'), 'GetLocaleInfoEx') <> nil, true));
//http://msdn2.microsoft.com/en-us/library/aa480152.aspx
Writeln('HKCU AppCompatFlags for ', ParamStr(0),': ',
DSiReadRegistry('Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers',
ParamStr(0), '', HKEY_CURRENT_USER));
Writeln('HKLM AppCompatFlags for ', ParamStr(0),': ',
DSiReadRegistry('Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers',
ParamStr(0), '', HKEY_LOCAL_MACHINE));
if not DSiIsWow64 then
Writeln('Not a 64-bit OS, KEY_WOW64_64KEY not tested')
else begin
//http://msdn.microsoft.com/library/en-us/sysinfo/base/about_the_registry.asp
Writeln('HKLM64 CurrentVersion registry value: ', DSiReadRegistry(
'\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'CurrentVersion', '',
HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE or KEY_WOW64_64KEY));
Writeln('HKLM64 ProductName registry value: ', DSiReadRegistry(
'\SOFTWARE\Microsoft\Windows NT\CurrentVersion', 'ProductName', '',
HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE or KEY_WOW64_64KEY));
Writeln('HKLM64 AppCompatFlags for ', ParamStr(0),': ',
DSiReadRegistry('Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers',
ParamStr(0), '', HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE or KEY_WOW64_64KEY));
end;
Writeln('DSiGetAppCompatFlags: ', DSiGetAppCompatFlags(ParamStr(0)));
Writeln('DSiGetTrueWindowsVersion: ', CDSiWindowsVersionStr[DSiGetTrueWindowsVersion]);
Readln;
end.

14 comments:

  1. Very useful! I have adopt your tips on My project

    I have no idea on detecting true OS version before i read this article.

    ReplyDelete
  2. Anonymous15:42

    Thanks from VisionSystems!

    ReplyDelete
  3. Very usefull, thanx!!

    Huflind [Netherlands]

    ReplyDelete
  4. Jackie14:02

    Still, this does not tell me why GetVersionEx function is sometime false! Can you elaborate on this? - Or may be i am missing something.

    ReplyDelete
  5. Don't understand. When is GetVersionEx false?

    ReplyDelete
  6. Andrew19:29

    What happens if MS introduce GetLocaleInfoEx in XP SP3 - due early 2008?

    ReplyDelete
  7. Then the code will have to be updated.

    Software is a never ending battle.

    ReplyDelete
  8. Andrew19:45

    Isn't it just! May not checking for the version of kernel32.dll be a more future-proof method ie, XP=V5.XXX and Vista=V6.XXX?

    ReplyDelete
  9. You could better change

    function ExportsAPI(module: HMODULE; const apiName: string): boolean;

    with:

    function ExportsAPI(module: HMODULE; const apiName: ansistring): boolean;

    That way it also compiles in Lazarus / Freepascal, which only support ansiString to PChar :).

    ReplyDelete
  10. The complete DSiWin32 library will have to be thoroughly checked for the Tiburon anyway. So, yes, this is planned for.

    ReplyDelete
  11. Anonymous17:02

    The only thing I can say after reading this is that you're now twice a failure you would be if you just relied on GetVersionEx and allowed the program to crash under emulation. Not only you're doing things which are not documented and not recommended in any way, circumventing normal version detection mechanisms and creating a possible source of mysterious errors if somebody somewhere has, say, new version of DLL on the old system, or if Microsoft decides to add one of those functions into older versions of system, or, or, in short, not only you're doing things so obviously against the rules.
    What's worse, you're thinking you're smarter than your users. Just think about it for a minute: if user willingly configures the application to run under the emulation layer, doesn't this mean he already understands the possible consequences (unstability etc) and STILL wants to continue? Why the heck would you make this task even more difficult to him than it would be without you?
    If only you knew the hatred towards you that burns in souls of those who're forced to launch their process-monitors and debuggers, scan through the actions of your application, detect your cheap tricks, write a shim libraries or something like that to trick your application into believing that you have guessed the version right, and after doing all of that STILL LAUNCH YOUR APP like they wished from the beginning. Just with much more pain, thanks to your help.

    ReplyDelete
  12. Don't use it then.

    And just one short remark - no, users have no idea what they're doing.

    ReplyDelete
  13. Anonymous17:53

    Ah, well, I'm not one of users of your program anyways, just stumbled upon this post by chance and was a bit shocked. But don't worry, real users will probably complain too ;)

    ReplyDelete
  14. No, they didn't. And now we fully support Vista, anyway, and this code is not active anymore.

    ReplyDelete