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()