DisconnectedContext MDA when calling WMI functions in single-threaded application
Asked Answered
D

1

10

I write an app in C#, .NET 3.0 in VS2005 with a feature of monitoring insertion/ejection of various removable drives (USB flash disks, CD-ROMs etc.). I did not want to use WMI, since it can be sometimes ambiguous (e.g. it can spawn multiple insertion events for a single USB drive), so I simply override the WndProc of my mainform to catch the WM_DEVICECHANGE message, as proposed here. Yesterday I run into a problem when it turned out that I will have to use WMI anyway to retrieve some obscure disk details like a serial number. It turns out that calling WMI routines from inside the WndProc throws the DisconnectedContext MDA.

After some digging I ended with an awkward workaround for that. The code is as follows:

    // the function for calling WMI 
    private void GetDrives()
    {
        ManagementClass diskDriveClass = new ManagementClass("Win32_DiskDrive");
        // THIS is the line I get DisconnectedContext MDA on when it happens:
        ManagementObjectCollection diskDriveList = diskDriveClass.GetInstances();
        foreach (ManagementObject dsk in diskDriveList)
        {
            // ...
        }
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // here it works perfectly fine
        GetDrives();
    }


    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);

        if (m.Msg == WM_DEVICECHANGE)
        {
            // here it throws DisconnectedContext MDA 
            // (or RPC_E_WRONG_THREAD if MDA disabled)
            // GetDrives();
            // so the workaround:
            DelegateGetDrives gdi = new DelegateGetDrives(GetDrives);
            IAsyncResult result = gdi.BeginInvoke(null, "");
            gdi.EndInvoke(result);
        }
    }
    // for the workaround only
    public delegate void DelegateGetDrives();

which basically means running the WMI-related procedure on a separate thread - but then, waiting for it to complete.

Now, the question is: why does it work, and why does it have to be that way? (or, does it?)

I don't understand the fact of getting the DisconnectedContext MDA or RPC_E_WRONG_THREAD in the first place. How does running GetDrives() procedure from a button click event handler differs from calling it from a WndProc? Don't they happen on the same main thread of my app? BTW, my app is completely single-threaded, so why all of the sudden an error referring to some 'wrong thread'? Does the use of WMI imply multithreading and special treatment of functions from System.Management?

In the meantime I found another question related to that MDA, it's here. OK, I can take it that calling WMI means creating a separate thread for the underlying COM component - but it still does not occur to me why no-magic is needed when calling it after a button is pressed and do-magic is needed when calling it from the WndProc.

I'm really confused about that and would appreciate some clarification on that matter. There are only a few worse things than having a solution and not knowing why it works :/

Cheers, Aleksander

Detached answered 13/10, 2010 at 7:43 Comment(1)
Same trouble here! I wish there was a solution. I'll add a bounty... maybe that will help.Contemptuous
S
6

There is a rather long discussion of COM Apartments and message pumping here. But the main point of interest is the message pump is used to ensure that calls in a STA are properly marshaled. Since the UI thread is the STA in question, messages would need to be pumped to ensure that everything works properly.

The WM_DEVICECHANGE message can actually be sent to the window multiple times. So in the case where you call GetDrives directly, you effectively end up with recursive calls. Put a break point on the GetDrives call and then attach a device to fire the event.

The first time you hit the break point, everything in fine. Now press F5 to continue and you will hit the break point a second time. This time the call stack is something like:

[In a sleep, wait, or join] DeleteMeWindowsForms.exe!DeleteMeWindowsForms.Form1.WndProc(ref System.Windows.Forms.Message m) Line 46 C# System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 bytes
System.Windows.Forms.dll!System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x31 bytes
System.Windows.Forms.dll!System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x64 bytes [Native to Managed Transition]
[Managed to Native Transition]
mscorlib.dll!System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x2b bytes mscorlib.dll!System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) + 0x2d bytes
mscorlib.dll!System.Threading.WaitHandle.WaitOne() + 0x10 bytes System.Management.dll!System.Management.MTAHelper.CreateInMTA(System.Type type) + 0x17b bytes
System.Management.dll!System.Management.ManagementPath.CreateWbemPath(string path) + 0x18 bytes System.Management.dll!System.Management.ManagementClass.ManagementClass(string path) + 0x29 bytes
DeleteMeWindowsForms.exe!DeleteMeWindowsForms.Form1.GetDrives() Line 23 + 0x1b bytes C#

So effectively the window messages are being pumped to ensure the COM calls are properly marshalled, but this has the side effect of calling your WndProc and GetDrives again (as there are pending WM_DEVICECHANGE messages) while still in a previous GetDrives call. When you use BeginInvoke, you remove this recursive call.

Again, put a breakpoint on the GetDrives call and press F5 after the first time it's hit. The next time around, wait a second or two then press F5 again. Sometimes it will fail, sometimes it won't and you'll hit your breakpoint again. This time, your callstack will include three calls to GetDrives, with the last one triggered by the enumeration of the diskDriveList collection. Because again, the messages are pumped to ensure the calls are marshaled.

It's hard to pinpoint exactly why the MDA is triggered, but given the recursive calls it reasonable to assume the COM context may be torn down prematurely and/or an object is collected before the underlying COM object can be released.

Sovereignty answered 14/4, 2011 at 2:2 Comment(7)
I am slowly starting to understand, so bear with me. Basically, you're saying that the call to GetDrives() requires WndProc on his form to be running? I don't understand how this is an issue, especially since he allows the base to handle it first. GetDrives() won't get called again, because he is testing for message type first, yes? Can you elaborate a bit more, or point me in the right direction? Sorry for my confusion. Thanks!Contemptuous
@Contemptuous - No problem. If you build a sample that uses code like he has above, you will see a similar stack trace to the one in my answer. You can see GetDrives is at the bottom. Also remember that I captured that stack trace after my break point on the GetDrives call was hit. So it's about to go into another GetDrives call.Sovereignty
@Contemptuous - There are multiple WM_DEVICECHANGE messages being sent. So the first time WndProc is called, it handles the first of such messages. The GetDrives call pumps the messages in order to marshal any COM calls into the STA thread (such as the return values from the WMI objects). Since there are more WM_DEVICECHANGE messages waiting to be processed, pumping the message queue will force these to be pushed through the WndProc override. Thus the recursion.Sovereignty
@CodeNaked, I think I get it. So normally when I do something with WndProc, it blocks until I am done, and then WndProc is called again for any other messages in the queue. To marshal for WMI objects, WndProc is necessary. So if I use WMI in WndProc, we run into trouble because WndProc is blocked, but needs to run for WMI to function. Is my terrible summary in the ballpark of correct? Thanks again. I see now that the solution is to not using anything requiring marshaling in WndProc.Contemptuous
@Contemptuous - You got it. Normally things block and you are ok. In this case it is technically blocking the first loop that is pumping the message queue, but another loop is started to pump the queue due to the COM marshaling.Sovereignty
@CodeNaked, cool, thank you! I have a similar problem over at #5657810, and I will use Aleksander's method to get around it, or some other asynchronous way.Contemptuous
Guys, thank you both very much - @Sovereignty for the explanation, @Contemptuous for the bounty offer ;-) and the discussion, you asked all the questions I would ask and now I also get it. Happy to see that this question helped someone else too. Cheers, A.Detached

© 2022 - 2024 — McMap. All rights reserved.