December 2006 - Posts

To all of my blog readers: a happy 2007 with great technological innovation, blogging, community building, events, big ideas and even bigger solutions.

From my side, stay tuned for more daily blogging beginning in February and more technical news.

2007, here we come!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

I couldn't believe my own eyes when reviewing the blog stats of yesterday: a 90% overall increase in the number of requests. Impossible at first sight, but here's the explanation: a link on MSDN's Windows Vista homepage to my article on Application Recovery :o.

Update: December 28, 2006 finished with a total number of 150,979 page requests. Compared to an average of 20,192 page requests per day this is too good to be true (647% increase). For the overall month score, there's already a growth of 50% compared to November. Seems like my New Year's gift will be additional bandwidth at my hosting company...

Apparently the "other side" was reading this page too, quoting the following phrase:

Although it's always better to fix the bugs, a smooth "crashing experience" [...] also helps to improve the end-user experience.

What I want to stress on is that the Application Recovery API is a generic OS-level API that controls how Windows responds to the event of an application that crashes. In the past, error reporting and immediate process termination was the only possible outcome. Now developers have the tools at hand to control what happens: application restart and a recovery opportunity before process termination.

In the end, who wants to bet his or her software to be 100% bug free? Think of the number of dependencies software rely on nowadays ... lots of places where something might go wrong you didn't account for.

Note: Others mention that lots of software packages in the past had the same functionality, allowing for document recovery etc. That's true of course, but with the Application Recovery API this kind of functionality becomes more available for all sorts of developers and all sorts of applications. The whole point is that existing software packages with recovery functionality have to employ some strategy in the code to make this possible, e.g. by working on temporary files (cf. Word) that can be recovered when the app is restarted. Or by some kind of auto-save functionality (I recall what I read in some manual 10 years or so ago: "auto-save prevents loosing information when the computer's power supply fails or electricity outage occurs"). When programming with the Application Recovery API, action is taken right after the moment the software crashed, allowing you to do recovery from the inside before the process is terminated. In lots of cases, you'll just grab some internal data structure and persist it to disk. It all depends on the kind of app.

Last but not least, it's a tradeoff once more. Do you want to go through the burden of writing possible complex recovery mechanisms, allowing to "clean up" the "normal" code paths (i.e. eliminating the "aspect" of recovery allowance thoughout the code), or do you trust the software to be bug-free enough, or maybe you just don't care about crashes (but you still might to want to use the Application Recovery API just to restart the app automatically after a possible crash, just like Windows Services can be restarted automatically by the SCM after unexpected termination).

After all, it's about fighting Finagle's law (wouldn't that be even nicer to quote me on?). And under any circumstance, including the recovery itself, something might go wrong.

Just my .314 cents.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Readers of my blog do know my personal interest in this master piece of Donald Knuth (shame of you if you don't know the name). The least you can say is that TAOCP is a long-running project, so I'm always excited to see evolution. A couple of days ago Mr. Knuth published a few "pre-fascicles" (call it a CTP or beta - whatever you want) of Volume 4 (see news page for accurate links):

  • Boolean Basics
  • Boolean Evaluation
  • Bitwise Tricks and Techniques

So, if you're interested in some core fundamentals of computer science, go ahead and read it. I hope to find enough time next month to read it in more depth myself. I assume you already have your copy of volumes 1 to 3 on your shelf. If you've been playing with the MIX language (before being replaced by MMIX), there's a correlation with .NET as well, through Rutger's MixEmul.

Note: The format of the file is .ps, download Ghostscript and GSview to open it.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Finally, it's here: download now.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

In the previous post about TxR, we covered how to interact with the KTM (Kernel Transaction Manager) directly from managed code and how to perform transactional registry operations, illustrated by using RegDeleteKeyTransacted. In today's post, we'll bring the DTC (Distributed Transaction Coordinator) and System.Transactions into play, which will allow us to do things like this:

using (TransactionScope tx = new TransactionScope()) { DeleteKey(HKey.HKEY_CURRENT_USER, key1); DeleteKey(HKey.HKEY_CURRENT_USER, key2); }

Needless to say, because of the role of the DTC, we can perform other transactional operations in the same scope and make all of these either complete together or not. Examples include the use of TxF in the same transaction scope, or database operations on SQL Server, or MSMQ stuff, or ... well whatever you might think of.

The code

Below, you can find a piece of sample code on how you might go ahead and create a very basic TxR library. The focus of this post will be on the System.Transactions stuff you have to take care of and the low-level plumbing to involve the DTC:

1 using System; 2 using System.Runtime.InteropServices; 3 using System.IO; 4 using Microsoft.Win32; 5 using System.Transactions; 6 7 namespace TxR 8 { 9 class Program 10 { 11 #region Transactional Registry operations 12 13 [DllImport("advapi32.dll", EntryPoint="RegDeleteKeyTransactedW")] 14 static extern long RegDeleteKeyTransacted(uint hkey, [MarshalAs(UnmanagedType.LPWStr)]string subkey, RegSam sam, uint reserved, IntPtr transaction, IntPtr reserved2); 15 16 enum HKey : uint 17 { 18 HKEY_CURRENT_USER = 0x80000001 19 } 20 21 [DllImport("Kernel32.dll")] 22 static extern bool CloseHandle(IntPtr handle); 23 24 enum RegSam : uint 25 { 26 KEY_WOW64_32KEY = 0x0200, 27 KEY_WOW64_64KEY = 0x0100 28 } 29 30 [ComImport] 31 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] 32 [Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")] 33 internal interface IKernelTransaction 34 { 35 void GetHandle([Out] out IntPtr handle); 36 } 37 38 static void DeleteKey(HKey hkey, string subkey) 39 { 40 if (Transaction.Current != null) 41 { 42 IKernelTransaction tx = (IKernelTransaction)TransactionInterop.GetDtcTransaction(Transaction.Current); 43 IntPtr txh; 44 tx.GetHandle(out txh); 45 46 if (txh == IntPtr.Zero) 47 throw new Exception(); //Q-n-D 48 49 if (RegDeleteKeyTransacted((uint)hkey, subkey, RegSam.KEY_WOW64_32KEY, 0, txh, IntPtr.Zero) != 0) 50 throw new Exception(); //Q-n-D 51 52 CloseHandle(txh); 53 } 54 else 55 { 56 // 57 // Perform non-transacted delete using Microsoft.Win32 APIs. 58 // 59 } 60 } 61 62 #endregion 63 64 static void Main(string[] args) 65 { 66 // 67 // Demo setup. 68 // 69 string key1 = "RegDeleteKeyTransactedDemo_01"; 70 string key2 = "RegDeleteKeyTransactedDemo_02"; 71 Registry.CurrentUser.CreateSubKey(key1); 72 Registry.CurrentUser.CreateSubKey(key2); 73 74 // 75 // Start the demo. 76 // 77 Console.WriteLine("Press <ENTER> to start the transaction."); 78 Console.ReadLine(); 79 80 // 81 // Make it transacted. 82 // 83 using (TransactionScope tx = new TransactionScope()) 84 { 85 // 86 // Delete the keys (transacted). 87 // 88 DeleteKey(HKey.HKEY_CURRENT_USER, key1); 89 DeleteKey(HKey.HKEY_CURRENT_USER, key2); 90 91 // 92 // Commit or rollback? 93 // 94 char c; 95 do 96 { 97 Console.WriteLine("{0} {1}.", key1, Registry.CurrentUser.OpenSubKey(key1) != null ? "still exists" : "has vanished"); 98 Console.WriteLine("{0} {1}.", key2, Registry.CurrentUser.OpenSubKey(key2) != null ? "still exists" : "has vanished"); 99 Console.Write("Commit transaction (Y/N)? "); 100 c = (char)Console.Read(); 101 } 102 while (c != 'Y' && c != 'y' && c != 'N' && c != 'n'); 103 104 if (c == 'Y' || c == 'y') 105 tx.Complete(); 106 } 107 } 108 } 109 }

The "library" is implemented in lines 11 to 62. First of all, compared to the code in the previous post, observe the omission of the KTM-related functions. Instead, we'll rely on the DTC to do the plubming behind the scenes to create the transaction. We only need to obtain a handle to the transaction to get ahead and call the RegDeleteKeyTransacted function in line 49.

A few remarks:

  • Line 14 has remained unchanged compared to the original version. It just declares our much desired demo function RegDeleteKeyTransacted.
  • Lines 16 to 19 define a simple enum which of course needs to be extended to be really useful. Basically, I just wanted to encapsulate HKEY_CURRENT_USER in a an enum that will be used further on as a parameter to indicate the root hive when calling our DeleteKey function.
  • Lines 30 to 36 are used to obtain a handle to the KTM transaction through DTC. This IKernelTransaction interface is new in Windows Vista and is used to build the bridge between DTC and the KTM for transactional Windows API. It has only one method called GetHandle described as follows in the SDK: Returns a handle that represents the transaction and can be passed as a parameter to transacted Windows APIs.
  • One line 40, we investigate whether or not we're already involved in a System.Transactions transaction (by means of a TransactionScope that is):
    • If not, we'll just perform the operation without using a transaction (line 56-59) using Microsoft.Win32 (you can supply this code yourself; I didn't want to do it here because it'd need a switch over different HKey values, which still needs to be extended).
    • Otherwise, in line 42, the IKernelTransaction::GetHandle function gets called. Luckily, the System.Transactions.TransactionInterop class has a static method GetDtcTransaction defined that can obtain the low-level DTC transaction corresponding to a System.Transactions transaction. The handle is obtained in lines 43 and 44.
      • If no valid handle is retrieved, we throw an exception, which on the caller side of our function will terminate the transaction in flight (when exceptions are thrown through the course of a TransactionScope, it rolls back).
      • Otherwise, the RegDeleteKeyTransacted function is called on line 49. Again, if something goes wrong, we throw an exception (which is done in a quick-n-dirty way for demo purposes; you'll need to provide more information through a self-written exception to do it the right way).
    • On line 52, the transaction's handle is closed using CloseHandle. Again, code needs to be polished to be ready for production and to make sure this handle is closed under all circumstances. SafeHandles might be your friends to do this (which I'll blog about later).

On the consumer side, not much has changed. Again we do create two keys for demo purposes (on lines 69 to 72). Next, in the System.Transactions.TransactionScope transaction scope (lines 83 to 106), the keys are deleted. If one of the deletions fails, an exception is thrown by our DeleteKey function and the transaction will roll back. Otherwise, execution reaches line 94, triggering the end-user decision logic, which ultimately calls Complete for commit the transaction scope if the user decides to do so.

Conclusion

All of the magic done in DeleteKey makes the use of the KTM and transactional APIs completely invisible for the managed code programmers that can rely on the comfortable System.Transactions namespace. Just what we needed.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

Last month I blogged about TxF in Windows Vista and how to use it from your own application to perform transactional file operations. You can find both posts over here:

Today we'll take a look at TxR brother, TxR which stands for Transactional Registry, a brand new feature in Windows Vista (and Longhorn Server) that makes it possible to perform registry operations in a transactional manner. A typical usage scenario might be a setup application that's performing a bunch of file operations (using TxF) and registry operations (using TxR) which can either be committed all together or rolled back in case something went wrong (or the user cancelled the installer).

The next two paragraphs are borrowed from my orginal TxF posts, but adapted for TxR and to reflect the situation in Windows Vista RTM.

Your new friends are *Transacted

Probably you know about the Reg* functions in the Windows API, like RegDeleteKey, RegDeleteKeyEx, RegCreateKeyEx, amongst many others (take a look at the MSDN documentation for a full list). All these functions remain unchanged in Windows Vista, with the same semantics.

However, if you want to perform transactional registry operations, a new set of functions is in place to help you out: Reg*Transacted, for example RegDeleteKeyTransacted and RegOpenKeyTransacted (which makes all subsequent operations performed against that key - with classic functions to set values and stuff). All of the transactional functions rely on the Kernel Transaction Manager (KTM) which manages transactions in the operating system. As an example, consider the following RegDeleteKeyTransacted function which has a pretty simple signature:

LONG RegDeleteKeyTransacted( HKEY hKey, LPCTSTR lpSubKey, REGSAM samDesired, DWORD Reserved, HANDLE hTransaction, PVOID pExtendedParameter );

The last two parameters are the ones used to make the operation transactional: hTransaction passes a handle to the KTM transaction, the pExtendedParameter is currently reserved and has to be set to NULL.

The KTM in a few words

On to the KTM stuff right now. As mentioned earlier, KTM stands for Kernel Transaction Manager. Don't get confused by the word kernel, it only refers to the fact the KTM's transaction engine is in the kernel. This doesn't imply the transaction can only run in kernel mode (i.e. KTM-transactions can be used in kernel and user mode) nor does it mean the transaction would be machine-local (i.e. the transaction is DTC-able). On Vista and Longhorn Server, the KTM is in charge of TxF (Transactional NTFS) and TxR (Transactional Registry).

Important remark: Today we walk the low-level path, interacting directly with the KTM. This is not the ideal way but just a mind-setter for follow-up posts. Later on, we'll bring System.Transactions on stage to interact with the KTM transaction.

Warning: Do read the Windows SDK documentation carefully before working with TxR operations, especially the RegOpenKeyTransacted function information concerning the pick-up of the active transaction by other functions subsequently.

A demo

Let's start by showing you the code right away:

1 using System; 2 using System.Runtime.InteropServices; 3 using System.IO; 4 using Microsoft.Win32; 5 6 namespace TxR 7 { 8 class Program 9 { 10 [DllImport("advapi32.dll", EntryPoint="RegDeleteKeyTransactedW")] 11 static extern long RegDeleteKeyTransactedW(uint hkey, [MarshalAs(UnmanagedType.LPWStr)]string subkey, RegSam sam, uint reserved, IntPtr transaction, IntPtr reserved2); 12 13 static uint HKEY_CURRENT_USER = 0x80000001; 14 15 [DllImport("Kernel32.dll")] 16 static extern bool CloseHandle(IntPtr handle); 17 18 [DllImport("Ktmw32.dll")] 19 static extern bool CommitTransaction(IntPtr transaction); 20 21 [DllImport("Ktmw32.dll")] 22 static extern bool RollbackTransaction(IntPtr transaction); 23 24 [DllImport("Ktmw32.dll")] 25 static extern IntPtr CreateTransaction(IntPtr securityAttributes, IntPtr guid, int options, int isolationLevel, int isolationFlags, int milliSeconds, string description); 26 27 static void Main(string[] args) 28 { 29 // 30 // Demo setup. 31 // 32 string key1 = "RegDeleteKeyTransactedDemo_01"; 33 string key2 = "RegDeleteKeyTransactedDemo_02"; 34 Registry.CurrentUser.CreateSubKey(key1); 35 Registry.CurrentUser.CreateSubKey(key2); 36 37 // 38 // Start the demo. 39 // 40 Console.WriteLine("Press <ENTER> to start the transaction."); 41 Console.ReadLine(); 42 43 // 44 // Create a kernel transaction. 45 // 46 IntPtr tx = CreateTransaction(IntPtr.Zero, IntPtr.Zero, 0, 0, 0, 0, null); 47 48 // 49 // Delete the keys (transacted). 50 // 51 bool rollback = false; 52 if (RegDeleteKeyTransacted(HKEY_CURRENT_USER, key1, RegSam.KEY_WOW64_32KEY, 0, tx, IntPtr.Zero) != 0) 53 rollback = true; 54 if (RegDeleteKeyTransacted(HKEY_CURRENT_USER, key2, RegSam.KEY_WOW64_32KEY, 0, tx, IntPtr.Zero) != 0) 55 rollback = true; 56 57 // 58 // Commit or rollback? 59 // 60 if (!rollback) 61 { 62 char c; 63 do 64 { 65 Console.WriteLine("{0} {1}.", key1, Registry.CurrentUser.OpenSubKey(key1) != null ? "still exists" : "has vanished"); 66 Console.WriteLine("{0} {1}.", key2, Registry.CurrentUser.OpenSubKey(key2) != null ? "still exists" : "has vanished"); 67 Console.Write("Commit transaction (Y/N)? "); 68 c = (char)Console.Read(); 69 } 70 while (c != 'Y' && c != 'y' && c != 'N' && c != 'n'); 71 72 if (c == 'Y' || c == 'y') 73 CommitTransaction(tx); 74 else 75 RollbackTransaction(tx); 76 } 77 else 78 { 79 Console.WriteLine("Forced rollback!"); 80 RollbackTransaction(tx); 81 } 82 83 // 84 // Close kernel mode transaction handle. 85 // 86 CloseHandle(tx); 87 } 88 89 enum RegSam : uint 90 { 91 KEY_WOW64_32KEY = 0x0200, 92 KEY_WOW64_64KEY = 0x0100 93 } 94 } 95 }

So, what's going on in here? Some explanation:

  • Lines 19, 22 and 25 contain the KTM functions to create a transaction (CreateTransaction), commit a transaction (CommitTransaction) and rollback a transaction (RollbackTransaction). More information can be found in the Windows SDK.
  • On line 11, the RegDeleteKeyTransacted function is declared with the parameters aforementioned in this post. The handle retrieved from CreateTransaction has to be past as the 5th parameter. Notice the EntryPoint property of the DllImport attribute to choose the Unicode version of the function, as well as the LPWStr marshal type for the second parameter. (All of this is done for clarity and explicitness to see the relation between the Windows API and the interop code.)
  • To use the HKCU hive of the registry, we're declaring the symbolic constant HKEY_CURRENT_USER on line 13. This will be the first parameter to our RegDeleteKeyTransacted call subsequently.
  • Next, lines 32 to 35 create two registry keys under HKCU for further demonstration purposes. These are using the non-transactional APIs which have been wrapped in the Microsoft.Win32 managed code namespace's RegistryKey class.
  • One line 46, the real work starts by creating a KTM transaction using a call to CreateTransaction. We keep things simple by passing a lot of default values. For more information, take a look at the SDK.
  • Lines 52 and 54 perform the RegDeleteKeyTransacted calls. If those return an error code (non-zero), we set the rollback boolean value to true, to indicate that not all operations succeeded and the transaction has to be rolled back (if that's the behavior you want if any of the operations fails, you might want to do more error code analysis to make this decision too of course).
    • If the transaction has to be rolled back, line 80 does that work by calling RollbackTransaction.
    • Otherwise, we leave the decision to the user to either commit (CommitTransaction) or rollback.
  • Finally, let's clean up things an close the handle used for the KTM transaction on line 86 by calling CloseHandle.

In action

Let's show a few pictures on a "breakpoint" basis. If the code reaches the specified line, the displayed screenshot (click to enlarge) will reflect the actual situation in the registry:

  • Line 28 - the demo keys haven't been created yet:


  • Line 40 - the demo keys are there:


  • Line 67 - the transactional delete operations have been executed but because of the ACID Isolation property, nothing is visible yet for the outside world:

       
  • Line 73 - the user decided to commit the transaction, now the keys are gone:

What's next?

In part 2 we'll take a look at how we can use the DTC and the System.Transactions namespace to enroll the KTM transaction in a distributed transaction and how to encapsulate the low level plumbing in a little library. Again, we'll focus on the RegDeleteKeyTransacted stuff because of its simplicity. In a later post, we might take a look at other registry operations too, which - however - need a bit more work regarding various parameters and would take the focus off the transactional stuff we're covering. The ultimate solution would be a change to Microsoft.Win32 to incorporate transactional support in Windows Vista and higher.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Yesterday I published a blog post about the WinSAT API in Windows Vista. It's always great to see others referring to posts and providing comments and suggestions, as this was the case with AddressOf.com. However, a few reactions on the reactions:

I really distaste the idea of "unsafe" code unless there is absolutely no way around using it... and even then I'd probably choose to implement a solution using C/C++ if that was the case.

What unsafe code is concerned, if you know what you're doing there's no real problem but I agree it's better to avoid it whenever you can. I didn't want to go through the whole burden of explaining GCHandle, pinning, etc and for sake of the demo I just took advantage of some simple pointer stuff. So, I certainly wouldn't exchange one line of unsafe code for a complete makeover in C/C++ unmanaged code. In that case I wouldn't have written the article at all since the Windows SDK has all the info C/C++ devs need. Nevertheless, thanks for pointing to the alternative using GCHandle.Alloc. I won't ever pronounce the word unsafe code anymore :p.

I also didn't like the fact that he was using GetEnumerator instead of using the much more friendly foreach functionality and because he was using GetEnumerator, he was having to do some unnecessary casting in the process.

Then tell me how to foreach over an enumeration without casting. Okay, I could just have duplicated the code five times, like this, which would be more explicit:

IProvideWinSATAssessmentInfo i; i = q.Info.GetAssessmentInfo(WINSAT_ASSESSMENT_TYPE.WINSAT_ASSESSMENT_CPU); Console.WriteLine(format, i.Title, i.Description, i.Score); i = q.Info.GetAssessmentInfo(WINSAT_ASSESSMENT_TYPE.WINSAT_ASSESSMENT_D3D); Console.WriteLine(format, i.Title, i.Description, i.Score); i = q.Info.GetAssessmentInfo(WINSAT_ASSESSMENT_TYPE.WINSAT_ASSESSMENT_DISK); Console.WriteLine(format, i.Title, i.Description, i.Score); i = q.Info.GetAssessmentInfo(WINSAT_ASSESSMENT_TYPE.WINSAT_ASSESSMENT_GRAPHICS); Console.WriteLine(format, i.Title, i.Description, i.Score); i = q.Info.GetAssessmentInfo(WINSAT_ASSESSMENT_TYPE.WINSAT_ASSESSMENT_MEMORY); Console.WriteLine(format, i.Title, i.Description, i.Score);

In the end we're talking about 5 casts; no big deal for a demo app. Difference in perf? Maybe 5 to 10 ms I guess; the relatively slow API calls overshadow the whole thing. Or maybe the source of the confusion is the classic misconception that the method A1 in the following piece of code would be more efficient than method A2:

private static void A1(CQueryWinSATClass q, string format) { foreach (WINSAT_ASSESSMENT_TYPE wat in Enum.GetValues(typeof(WINSAT_ASSESSMENT_TYPE))) { IProvideWinSATAssessmentInfo i = q.Info.GetAssessmentInfo(wat); Console.WriteLine(format, i.Title, i.Description, i.Score); } } private static void A2(CQueryWinSATClass q, string format) { IEnumerator e = Enum.GetValues(typeof(WINSAT_ASSESSMENT_TYPE)).GetEnumerator(); while (e.MoveNext()) { IProvideWinSATAssessmentInfo i = q.Info.GetAssessmentInfo((WINSAT_ASSESSMENT_TYPE)e.Current); Console.WriteLine(format, i.Title, i.Description, i.Score); } }

The point is that GetValues returns an Array, which contains elements of type System.Object, which in case of enums introduces boxing to occur. What happens in A1 is that the foreach loop is translated in similar logic as in A2, which is iterating over the (non-generic) IEnumerator returned by calling System.Array::GetEnumerator. Inside the loop, the IEnumerator's Current property will be retrieved and unboxed to the enum type. Because my blog readers like IL, I've included this analysis below. For A1 take a look at lines IL_001a and IL_001f, for A2 look at IL_0020 and IL_0025.

.method private hidebysig static void A1(class [Interop.WINSATLib]WINSATLib.CQueryWinSATClass q, string format) cil managed { // Code size 122 (0x7a) .maxstack 4 .locals init ([0] valuetype [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE wat, [1] class [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo i, [2] class [mscorlib]System.Collections.IEnumerator CS$5$0000, [3] bool CS$4$0001, [4] class [mscorlib]System.IDisposable CS$0$0002) IL_0000: nop IL_0001: nop IL_0002: ldtoken [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE IL_0007: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) IL_000c: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type) IL_0011: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator() IL_0016: stloc.2 .try { IL_0017: br.s IL_0052 IL_0019: ldloc.2 IL_001a: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current() IL_001f: unbox.any [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE IL_0024: stloc.0 IL_0025: nop IL_0026: ldarg.0 IL_0027: callvirt instance class [Interop.WINSATLib]WINSATLib.IProvideWinSATResultsInfo [Interop.WINSATLib]WINSATLib.CQueryWinSATClass::get_Info() IL_002c: ldloc.0 IL_002d: callvirt instance class [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo [Interop.WINSATLib]WINSATLib.IProvideWinSATResultsInfo::GetAssessmentInfo(valuetype [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE) IL_0032: stloc.1 IL_0033: ldarg.1 IL_0034: ldloc.1 IL_0035: callvirt instance string [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Title() IL_003a: ldloc.1 IL_003b: callvirt instance string [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Description() IL_0040: ldloc.1 IL_0041: callvirt instance float32 [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Score() IL_0046: box [mscorlib]System.Single IL_004b: call void [mscorlib]System.Console::WriteLine(string, object, object, object) IL_0050: nop IL_0051: nop IL_0052: ldloc.2 IL_0053: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0058: stloc.3 IL_0059: ldloc.3 IL_005a: brtrue.s IL_0019 IL_005c: leave.s IL_0078 } // end .try finally { IL_005e: ldloc.2 IL_005f: isinst [mscorlib]System.IDisposable IL_0064: stloc.s CS$0$0002 IL_0066: ldloc.s CS$0$0002 IL_0068: ldnull IL_0069: ceq IL_006b: stloc.3 IL_006c: ldloc.3 IL_006d: brtrue.s IL_0077 IL_006f: ldloc.s CS$0$0002 IL_0071: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0076: nop IL_0077: endfinally } // end handler IL_0078: nop IL_0079: ret } // end of method Program::A1
.method private hidebysig static void A2(class [Interop.WINSATLib]WINSATLib.CQueryWinSATClass q, string format) cil managed { // Code size 90 (0x5a) .maxstack 4 .locals init ([0] class [mscorlib]System.Collections.IEnumerator e, [1] class [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo i, [2] bool CS$4$0000) IL_0000: nop IL_0001: ldtoken [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE IL_0006: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) IL_000b: call class [mscorlib]System.Array [mscorlib]System.Enum::GetValues(class [mscorlib]System.Type) IL_0010: callvirt instance class [mscorlib]System.Collections.IEnumerator [mscorlib]System.Array::GetEnumerator() IL_0015: stloc.0 IL_0016: br.s IL_004f IL_0018: nop IL_0019: ldarg.0 IL_001a: callvirt instance class [Interop.WINSATLib]WINSATLib.IProvideWinSATResultsInfo [Interop.WINSATLib]WINSATLib.CQueryWinSATClass::get_Info() IL_001f: ldloc.0 IL_0020: callvirt instance object [mscorlib]System.Collections.IEnumerator::get_Current() IL_0025: unbox.any [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE IL_002a: callvirt instance class [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo [Interop.WINSATLib]WINSATLib.IProvideWinSATResultsInfo::GetAssessmentInfo(valuetype [Interop.WINSATLib]WINSATLib.WINSAT_ASSESSMENT_TYPE) IL_002f: stloc.1 IL_0030: ldarg.1 IL_0031: ldloc.1 IL_0032: callvirt instance string [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Title() IL_0037: ldloc.1 IL_0038: callvirt instance string [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Description() IL_003d: ldloc.1 IL_003e: callvirt instance float32 [Interop.WINSATLib]WINSATLib.IProvideWinSATAssessmentInfo::get_Score() IL_0043: box [mscorlib]System.Single IL_0048: call void [mscorlib]System.Console::WriteLine(string, object, object, object) IL_004d: nop IL_004e: nop IL_004f: ldloc.0 IL_0050: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext() IL_0055: stloc.2 IL_0056: ldloc.2 IL_0057: brtrue.s IL_0018 IL_0059: ret } // end of method Program::A2

As the matter in fact, the explicit enumeration logic using a while-loop might look less elegant, but it makes the casting explicit which is otherwise introduced by compiler magic. By the way, don't worry about the try-finally block in A1 which doesn't provide us with more value, since System.Array isn't IDisposable. (Btw, if you really want to optimize somewhat, take q.Info out of the loop.)

Finally, implementing this as a console application just seemed wrong somehow; especially since he was still showing a windows form from the console app to demonstrate the ability to garner the Windows Vista generated image representing the base score.

I always intend to make things as simple as possible (cf. Einstein's famous quote mentioned in this post), in this case the console app just does the trick; everyone knows you can put the whole stuff in a WinForms app too in a completely similar manner. The score display was the only aspect that required WinForms and the orginal implementation just invoked the Save method on the bitmap to store it as a .bmp file, like this:

if (t != IntPtr.Zero) Bitmap.FromHbitmap(t).Save("WinSATDemo.bmp");

It would have been even shorter, I agree. Ultimately, I decided to use WinForms instead to display the picture. For me, the focus is on retrieving the numbers, not cloning what's already available in Windows to display the scores. Lots of WinForms code hide the real intention of the app in quite some cases. Anyway, just my .314 cents.

(PS: Sorry I had to delay the real technical post of today, but I felt I had to put a reaction online.)

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Introduction

Time for another Windows SDK adventure in Windows Vista: enter the WinSAT API. WinSAT stands for Windows System Assessment Tool and is described as follows in the SDK:

Windows System Assessment Tool (WinSAT) assesses the performance characteristics and capabilities of a computer. Developers can use this API to develop software that can access the performance and capability information of the computer to determine the optimal application settings based on that computer's performance capabilities.

The capabilites also indicates what scenarios and applications will perform well on the computer. For example, if a software package contains a rating on its packaging, a user can use the rating on the software package and the computer's capability rating to determine if the package will run well on the computer.

I think a couple of screenshots will clarify a lot (click to enlarge):

The numeric scores like 3.7 are exactly what WinSAT delivers to you. The idea of WinSAT is to assess the system's capabilities using a simple numeric score: the higher, the better. This enables some scenarios:

  • Allowing end users to see the bottlenecks in the system; e.g. if the lowest score is the video card, that might be the first thing to consider for an upgrade.
  • Comparing machines to each other; also useful for computer manufactures to make promotion based on this score.
  • Software packages can indicate a minimal required score to run properly on the product box.
  • Programmers can query the various rating scores available to dynamically adapt the behavior of the app, for example by disabling the graphical effects if the graphical score is too low.

 

Retrieving WinSAT scores in managed code

In this post, we'll focus on retrieving WinSAT scores in managed code, in order to use these scores to make decisions in our application (bullet 3 in the list above). I'll only show how to retrieve scores, not how to interpret scores nor how to use it in decision making (which will be different in every app and should be relatively straightforward).

WinSAT is defined in %windir%\system32\WinSATAPI.dll as a COM-based library. Documentation is available in the Windows SDK and online on MSDN. We'll focus on the following interfaces:

  • IProvideWinSATResultsInfo - Gets information about an assessment, e.g. the Windows Capability rating (see screenshots above in the Introduction paragraph). We'll use this as our starting point. It contains properties to query the assessment state, assessment date and time, a friendly description of the assessment and the score information. Using the GetAssessmentInfo, detailed information can be obtained, represented as a IProvideWinSATAssessmentInfo object (see below).
  • IProvideWinSATAssessmentInfo - Gets information about an assessment (e.g. graphics, cpu, memory, disk, etc). Three properties are defined: Description, Score and Title.
  • IProvideWinSATVisuals - Used to retrieve a bitmap that represents a score graphically, as shown below. This image can be used in the application to present scores in a user-friendly and well-known fashion (since these images will appear on software packages, with downloads online, on computer systems, etc).

For sake of completeness, I've copied the winsatcominterface.h definitions of these interfaces below:

MIDL_INTERFACE("0CD1C380-52D3-4678-AC6F-E929E480BE9E")
IProvideWinSATAssessmentInfo : public IDispatch
{
public:
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_Score(
        /* [retval][out] */ __RPC__out float *score) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_Title(
        /* [string][retval][out] */ __RPC__deref_out_opt_string BSTR *title) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_Description(
        /* [string][retval][out] */ __RPC__deref_out_opt_string BSTR *description) = 0;
};

MIDL_INTERFACE("F8334D5D-568E-4075-875F-9DF341506640")
IProvideWinSATResultsInfo : public IDispatch
{
public:
    virtual /* [id] */ HRESULT STDMETHODCALLTYPE GetAssessmentInfo(
        /* [in] */ WINSAT_ASSESSMENT_TYPE assessment,
        /* [retval][out] */ __RPC__deref_out_opt IProvideWinSATAssessmentInfo **ppinfo) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_AssessmentState(
        /* [retval][out] */ __RPC__out WINSAT_ASSESSMENT_STATE *state) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_AssessmentDateTime(
        /* [retval][out] */ __RPC__out VARIANT *fileTime) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_SystemRating(
        /* [retval][out] */ __RPC__out float *level) = 0;
    virtual /* [propget][id] */ HRESULT STDMETHODCALLTYPE get_RatingStateDesc(
        /* [retval][out] */ __RPC__deref_out_opt BSTR *description) = 0;
};

MIDL_INTERFACE("A9F4ADE0-871A-42a3-B813-3078D25162C9")
IProvideWinSATVisuals : public IUnknown
{
public:
    virtual HRESULT STDMETHODCALLTYPE get_Bitmap(
        /* [in] */ WINSAT_BITMAP_SIZE bitmapSize,
        WINSAT_ASSESSMENT_STATE state,
        float rating,
        /* [out] */ __RPC__deref_out_opt HBITMAP *pBitmap) = 0;
};

Time to open Visual Studio 2005 and to create a Console Application in C#, named WinSATDemo. Next, add a few references:

  • To System.Drawing (for the "visuals" to convert the HBITMAP to a .NET BCL Bitmap).
  • To System.Windows.Forms (right, we'll do a little Windows Forms programming, but the core of the app will be a console app).
  • To the COM library for WinSAT in %windir%\system32\WinSATAPI.dll:

The result should look like this in the Solution Explorer:

Finally, you'll need to change the Build properties of the project to allow unsafe code (explanation will follow later):

On to the code now. We'll use the WINSATLib which was imported from a COM library which is relatively straightforward once you know the meaning of the various interfaces as well as the generated classes. Here we go:

1 using System; 2 using System.Collections; 3 using System.Drawing; 4 using System.Runtime.InteropServices; 5 using System.Windows.Forms; 6 using WINSATLib; 7 8 namespace WinSATDemo 9 { 10 /// <summary> 11 /// Demo of the WinSAT technology in Windows Vista. Shows how to retrieve system assessment information in managed code. 12 /// </summary> 13 class Program 14 { 15 /// <summary> 16 /// Entry point of the demo application. 17 /// </summary> 18 /// <remarks>A single-threaded apartment state is required for the COM interop. Don't remove the [STAThread] attribute.</remarks> 19 [STAThread] 20 static void Main() 21 { 22 // 23 // Provides access to assessment state. 24 // 25 CQueryWinSATClass q = new CQueryWinSATClass(); 26 27 // 28 // Check for valid state. 29 // 30 if (q.Info.AssessmentState == WINSAT_ASSESSMENT_STATE.WINSAT_ASSESSMENT_STATE_VALID 31 || q.Info.AssessmentState == WINSAT_ASSESSMENT_STATE.WINSAT_ASSESSMENT_STATE_INCOHERENT_WITH_HARDWARE) 32 { 33 // 34 // Some formatting stuff. 35 // 36 string format = "{0,-18} {1,-56} {2:N1}"; 37 string seperator = String.Format(format, new string('-', 18), new string('-', 56), new string('-', 3)); 38 39 // 40 // General rating information. 41 // 42 Console.WriteLine("{0} on {1}", q.Info.RatingStateDesc, q.Info.AssessmentDateTime); 43 Console.WriteLine(); 44 45 // 46 // Get ratings for individual assessment types. 47 // 48 Console.WriteLine(format, "Category", "Description", "Rat"); 49 Console.WriteLine(seperator); 50 IEnumerator e = Enum.GetValues(typeof(WINSAT_ASSESSMENT_TYPE)).GetEnumerator(); 51 while (e.MoveNext()) 52 { 53 IProvideWinSATAssessmentInfo i = q.Info.GetAssessmentInfo((WINSAT_ASSESSMENT_TYPE)e.Current); 54 Console.WriteLine(format, i.Title, i.Description, i.Score); 55 } 56 57 // 58 // Overall system base score. 59 // 60 Console.WriteLine(seperator); 61 Console.WriteLine(format, "Base score:", "Determined by lowest subscore", q.Info.SystemRating); 62 } 63 64 // 65 // Get bitmap with assessment rating figure in WinSAT style. 66 // 67 IntPtr t; 68 unsafe 69 { 70 /* 71 * get_Bitmap has the following unmanaged signature: 72 * HRESULT get_Bitmap( 73 * WINSAT_BITMAP_SIZE bitmapSize, 74 * WINSAT_ASSESSMENT_STATE state, 75 * float rating, 76 * HBITMAP* pBitmap 77 * ); 78 * where HBITMAP is defined as (WinDef.h): 79 * typedef HANDLE HBITMAP; 80 * 81 * The last parameter gets translated into an IntPtr which is the pointer (&t) to the pointer (t) to the bitmap. 82 */ 83 CProvideWinSATVisualsClass v = new CProvideWinSATVisualsClass(); 84 v.get_Bitmap(WINSAT_BITMAP_SIZE.WINSAT_BITMAP_SIZE_NORMAL, q.Info.AssessmentState, q.Info.SystemRating, new IntPtr(&t)); 85 } 86 87 // 88 // Display a quick-n-dirty dialog box with the WinSAT assessment rating bitmap. 89 // 90 if (t != IntPtr.Zero) 91 { 92 Form frm = new Form(); 93 frm.Text = "WinSAT Bitmap"; 94 PictureBox pict = new PictureBox(); 95 pict.SizeMode = PictureBoxSizeMode.AutoSize; 96 pict.Image = Bitmap.FromHbitmap(t); 97 frm.Controls.Add(pict); 98 frm.ShowDialog(); 99 } 100 } 101 } 102 }

A little explanation of various things:

  • Line 19 - [STAThread] - To use to COM library, you need to set this attribute, otherwise you'll end up with an InvalidCastException, like this: Unable to cast COM object of type 'WINSATLib.CQueryWinSATClass' to interface type 'WINSATLib.IQueryRecentWinSATAssessment'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{F8AD5D1F-3B47-4BDC-9375-7C6B1DA4ECA7}' failed due to the following error: No such interface supported (Exception from HRESULT: 0x80004002 (E_NOINTERFACE)).
  • Line 25 - CQueryWinSATClass - This class provides access to the "recent" WinSAT assessment results, it's defined as follows by the COM import and implements IQueryRecentWinSATAssessment:
  • using System; using System.Runtime.InteropServices; namespace WINSATLib { [TypeLibType(2)] [ClassInterface(0)] [Guid("F3BDFAD3-F276-49E9-9B17-C474F48F0764")] public class CQueryWinSATClass : IQueryRecentWinSATAssessment, CQueryWinSAT { public CQueryWinSATClass(); [DispId(2)] public virtual IProvideWinSATResultsInfo Info { get; } [DispId(1)] public virtual IXMLDOMNodeList get_xml(string xPath, string namespaces); } }

    Notice the get_xml stuff which can be used to query the XML describing the assessment. Basically, assessments are kept as XML documents in %windir%\Performance\WinSAT\DataStore which can be used to query much much more detailed information about the system. I've uploaded my system's assessment information (ran during post-setup phase of Vista installation) as an example over here.

  • Line 30, 31 - WINSAT_ASSESSMENT_STATE - This enumeration contains possible states of the assessment that was queried (through q.Info, which is of type IProvideWinSATResultsInfo). Information in the SDK tells the following about the various values:

    WINSAT_ASSESSMENT_STATE_MIN
  • Minimum enumeration value for this enumeration. WINSAT_ASSESSMENT_STATE_UNKNOWN The current state of the assessment is unknown. WINSAT_ASSESSMENT_STATE_VALID The current assessment data is valid for the current computer configuration. WINSAT_ASSESSMENT_STATE_INCOHERENT_WITH_HARDWARE The hardware has changed configuration since the last time a formal assessment was run but the data is still present and should be displayed. WINSAT_ASSESSMENT_STATE_NOT_AVAILABLE No data is available because a formal WinSAT assessment has not been run on this computer. WINSAT_ASSESSMENT_STATE_INVALID The assessment data is not valid. WINSAT_ASSESSMENT_STATE_MAX Minimum enumeration value for this enumeration.

    which explains why we're only interested in the VALID and INCOHERENT_WITH_HARDWARE states. Needless to say, you'll need a rescue net for other cases as well.
  • Line 36, 37 - String formatting to display results in columns: {0,-18} means left alignment and padding to 18 characters in total; {2:N1} means displaying a numeric value with one decimal. This format string is used on line 37 to create a separator and on lines 54 and 61 to display assessment information in columns.
  • Line 42 - Over here the date/time of the assessment and the description are displayed.
  • Line 50-55 - WINSAT_ASSESSMENT_TYPE - Contains all types of assessments done by WinSAT, which can be used to query for subscore information using GetAssessmentInfo (line 53). The enumeration logic makes it possible to iterate over all possible enum values in a relatively straightforward fashion.
  • Line 61 - Displays the overall base score of the system, in a "result row" of our table.
  • Line 67-85 - Unsafe code to retrieve the bitmap with the score indicator using get_Bitmap. Recall the definition of the get_Bitmap method in the .h file:

        virtual HRESULT STDMETHODCALLTYPE get_Bitmap(
            /* [in] */ WINSAT_BITMAP_SIZE bitmapSize,
            WINSAT_ASSESSMENT_STATE state,
            float rating,
            /* [out] */ __RPC__deref_out_opt HBITMAP *pBitmap) = 0;


    The last parameter is an output parameter with a pointer to an HBITMAP, which on its turn is defined as a typedef (= synonym) for a HANDLE. Essentially, this means that we pass in a pointer to a pointer which will be changed by the WinSAT library to point to the bitmap in memory. Look at this piece of pseudocode if you don't fully get it:

    void *t = NULL; // t -> NULL
    void **p = &t; // p -> t

    getBitmap(bla, bla, bla, p); // now, for instance,  t -> 0x12345678 which contains the bitmap


    Internally, getBitmap does something like this:

    // lots of stuff omitted
    *pBitmap = pSomeBitmap; // pSomeBitmap is the pointer to the created bitmap


    We need unsafe code because we need to grab the address of a pointer (t in the pseudo-code above == t on line 67) which is done on line 84 using &t. If the call to get_Bitmap succeeds (not checked in the code; I know it's dirty coding), t will point to a location in memory where the HBITMAP lives.
  • Line 90 - Make sure the bitmap pointer was changed by the get_Bitmap call, so that we can display the image.
  • Line 92-98 - Straightforward Windows Forms code to show a form with a PictureBox control to display the image. Notice the conversion of the HBITMAP to a System.Drawing.Bitmap using the System.Drawing.Bitmap.FromHbitmap factory method.

This is what the result looks like on my machine (compare to the scores and image in the introduction pictures):

Code can be downloaded here.

Happy coding!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

The Belgian Developer & IT Pro Days 2007 conference is on its way. Notice there will be precons as well. From the website on www.dev-itprodays.be:

Save the Date for Developer & IT Pro Days 2007 in Ghent

Join us on March 28-29, 2007 in Ghent at the International Convention Center Ghent (ICC).

Developer & IT Pro Days 2007 Pricing & Registration
Registration will go live soon.
Registration rate is €299 for both conference days and $399 including pre-conference day.

Conference Agenda

  • Tuesday 27 March : Pre-Conference Seminars
  • Wednesday 28 March : Day 1
  • Thursday 29 March : Day 2

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Assume you have some registry key you want to delete in its entirety, meaning you want to delete it with all children and subtrees. A sample is depicted below (HKCU\MyDemo):

Chances are high you've tried to delete the key with the following piece of code (well, at least in some language or another using the RegDeleteKey function of advapi32):

using System;
using System.Runtime.InteropServices;

namespace RegDeleteTreeDemo
{
   class Program
   {
      [DllImport("advapi32.dll")]
      static extern int RegDeleteKey(uint hKey, string lpSubkey);

      static uint HKEY_CURRENT_USER = 0x80000001;

      static void Main(string[] args)
      {
         int res = RegDeleteKey(HKEY_CURRENT_USER, "MyDemo");
      }
   }
}

If you've tried this at home you'll know it isn't that easy. Effectively, it's like doing an rd on a non-empty directory. The Windows SDK mentions: "The subkey to be deleted must not have subkeys. To delete a key and all its subkeys, you need to enumerate the subkeys and delete them individually." and a sample is available that illustrates doing this.

There are however better alternatives if you really want to delete a whole key with all its descendants. There's a function called SHDeleteKey defined in SHLWAPI but in Windows Vista this functionality has been added to advapi32 as well using RegDeleteTree. A sample is shown below:

using System;
using System.Runtime.InteropServices;

namespace RegDeleteTreeDemo
{
   class Program
   {
      [DllImport("advapi32.dll")]
      static extern int RegDeleteTree(uint hKey, string lpSubkey);

      static uint HKEY_CURRENT_USER = 0x80000001;

      static void Main(string[] args)
      {
         int res = RegDeleteTree(HKEY_CURRENT_USER, "MyDemo");
      }
   }
}

It's as easy as this, and now the deletion works like a charm. So, time to get rid of recursive tree deletion stuff.

Warning: Extra care is recommended when working with this function; I'm not responsible for accidental deletions of things like HKCU\Software (haven't tried it myself, don't know whether it would work, expect it would, but don't want to know it at all).

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

More Posts Next page »