2010-10-13 18 views
10

Ich schreibe eine App in C#, .NET 3.0 in VS2005 mit einer Funktion zur Überwachung der Einfügung/Auswurf von verschiedenen Wechsellaufwerken (USB-Flash-Laufwerke, CD-ROMs etc.). Ich wollte WMI nicht verwenden, da es manchmal mehrdeutig sein kann (z. B. kann es mehrere Einfügungsereignisse für ein einzelnes USB-Laufwerk erzeugen), so überschreibe ich einfach den WndProc meines Hauptformulars, um die WM_DEVICECHANGE-Nachricht abzufangen, wie vorgeschlagen here. Gestern habe ich auf ein Problem gestoßen, als sich herausstellte, dass ich sowieso WMI verwenden muss, um einige obskure Disk-Details wie eine Seriennummer zu erhalten. Es stellt sich heraus, dass das Aufrufen von WMI-Routinen aus dem WndProc den DisconnectedContext-MDA auslöst.DisconnectedContext MDA beim Aufruf von WMI-Funktionen in Single-Thread-Anwendung

Nach etwas graben endete ich mit einem peinlichen Workaround dafür. Der Code lautet wie folgt:

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

was im Grunde bedeutet das WMI-bezogenen Verfahren auf einem separaten Thread ausgeführt wird - aber dann, warten auf sie vervollständigen zu.

Nun, die Frage ist: warum funktioniert es, und warum muss es so sein? (oder, oder?)

Ich verstehe nicht die Tatsache, den DisconnectedContext MDA oder RPC_E_WRONG_THREAD an erster Stelle zu bekommen. Wie unterscheidet sich das Ausführen der GetDrives()-Prozedur von einem Ereignishandler für das Klicken auf Schaltflächen nicht davon, dass es von einem WndProc aufgerufen wird? Passieren sie nicht im selben Hauptthread meiner App? Übrigens, meine App ist komplett single-threaded, warum also plötzlich ein Fehler, der sich auf einen "falschen Thread" bezieht? Bedeutet der Einsatz von WMI Multithreading und spezielle Behandlung von Funktionen von System.Management?

In der Zwischenzeit habe ich eine andere Frage zu diesem MDA gefunden, es ist here. OK, ich kann es so nehmen, dass das Aufrufen von WMI bedeutet, einen separaten Thread für die zugrunde liegende COM-Komponente zu erstellen - aber mir fällt immer noch nicht ein, warum no-magic beim Aufruf nach dem Drücken einer Taste benötigt wird und do-magic beim Aufruf benötigt wird es aus dem WndProc.

Ich bin wirklich verwirrt darüber und würde eine Klärung in dieser Angelegenheit schätzen. Es gibt nur wenige Dinge schlimmer als eine Lösung mit und ohne zu wissen, warum es funktioniert:/

Cheers, Aleksander

+1

Gleiche Probleme hier! Ich wünschte, es gäbe eine Lösung. Ich werde ein Kopfgeld hinzufügen ... vielleicht wird das helfen. – Brad

Antwort

6

Es ist eine ziemlich lange Diskussion über COM Apartments und Nachrichtenpump here. Aber der Hauptpunkt des Interesses ist die Nachricht Pumpe wird verwendet, um sicherzustellen, dass Anrufe in einer STA richtig gemarshallt sind. Da der UI-Thread die betreffende STA ist, müssten Nachrichten gepumpt werden, um sicherzustellen, dass alles ordnungsgemäß funktioniert.

Die WM_DEVICECHANGE Nachricht kann tatsächlich mehrmals an das Fenster gesendet werden. Wenn Sie also GetDrives direkt aufrufen, erhalten Sie effektiv rekursive Aufrufe. Legen Sie einen Unterbrechungspunkt für den GetDrives-Aufruf fest und hängen Sie dann ein Gerät an, um das Ereignis auszulösen.

Das erste Mal, wenn Sie den Breakpoint treffen, alles in Ordnung. Drücken Sie jetzt F5, um fortzufahren, und Sie werden den Haltepunkt ein zweites Mal treffen. Diesmal ist der Call-Stack ist so etwas wie:

[In einem Schlaf, warten, oder kommen] DeleteMeWindowsForms.exe DeleteMeWindowsForms.Form1.WndProc (ref System.Windows.Forms.Message m) Linie 46 C# ! System.Windows.Forms.dll! System.Windows.Forms.Control.ControlNativeWindow.OnMessage (Referenz System.Windows.Forms.Message m) + 0x13 Byte
System.Windows.Forms.dll! System.Windows.Forms.Control.ControlNativeWindow.WndProc (Referenz System.Windows.Forms.Message m) + 0x31 Byte
System.Windows.Forms.dll! System.Windows.Forms.NativeWindow.DebuggableCallback (System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x64 Bytes [zu Managed Transition Nativ]
[auf einheitlichem Transition Managed]
mscorlib.dll! System.Threading.WaitHandle.InternalWaitOne (System.Runtime .InteropServices.SafeHandle waitableSafeHandle, lange millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x2b Bytes mscorlib.dll! System.Threading.WaitHandle.WaitOne (int millisecondsTimeout, bool exitContext) + 0x2D Bytes
mscorlib.dll! System.Threading. WaitHandle.Wait One() + 0x10 bytes System.Management.dll! System.Management.MTAHelper.CreateInMTA (Typ System.Type) + 0x17b Byte
System.Management.dll! System.Management.ManagementPath.CreateWbemPath (Zeichenfolgenpfad) + 0x18 Bytes System.Management.dll! System.Management.ManagementClass.ManagementClass (string path) + 0x29 Bytes
DeleteMeWindowsForms.exe! DeleteMeWindowsForms.Form1.GetDrives() Linie 23 + 0x1b Bytes C#

So effektiv die Fenstermeldungen werden gepumpt, um sicherzustellen, dass die COM-Aufrufe ordnungsgemäß verwaltet werden. Dies hat jedoch den Nebeneffekt, dass Sie WndProc und GetDrives noch einmal aufrufen (da ausstehende WM_DEVICECHANGE-Nachrichten vorhanden sind) in einem vorherigen GetDrives-Aufruf. Wenn Sie BeginInvoke verwenden, entfernen Sie diesen rekursiven Aufruf.

Geben Sie erneut einen Haltepunkt für den GetDrives-Aufruf ein und drücken Sie nach dem ersten Treffer F5. Warten Sie ein oder zwei Sekunden und drücken Sie F5 erneut. Manchmal wird es scheitern, manchmal nicht und du wirst deinen Haltepunkt erneut treffen. Dieses Mal enthält Ihr Aufrufstack drei Aufrufe von GetDrives, von denen der letzte durch die Enumeration der diskDriveList-Auflistung ausgelöst wird. Weil die Nachrichten erneut gepumpt werden, um sicherzustellen, dass die Aufrufe gemarshallt werden.

Es ist schwer, genau zu ermitteln, warum die MDA ausgelöst wird, aber die rekursive Aufrufe gegeben es vernünftig, der COM-Kontext zu übernehmen zerrissen werden kann, vorzeitig nach unten und/oder ein Objekt gesammelt wird, bevor das zugrunde liegende COM-Objekt freigegeben werden kann.

+0

Ich beginne langsam zu verstehen, also ertragen Sie mit mir. Im Grunde sagen Sie, dass der Aufruf von GetDrives() erfordert, dass WndProc auf seinem Formular ausgeführt wird? Ich verstehe nicht, wie das ein Problem ist, besonders da er es der Basis erlaubt, zuerst damit umzugehen. GetDrives() wird nicht erneut aufgerufen, weil er zuerst nach Nachrichtentyp testet, ja? Können Sie etwas mehr ausarbeiten, oder weisen Sie mich in die richtige Richtung? Entschuldigung für meine Verwirrung. Vielen Dank! – Brad

+0

@Brad - Kein Problem. Wenn Sie ein Beispiel erstellen, das Code wie oben verwendet, werden Sie einen ähnlichen Stack-Trace wie in meiner Antwort sehen. Sie können sehen, GetDrives ist am unteren Rand. Denken Sie auch daran, dass ich diese Stack-Trace erfasst habe, nachdem mein Break-Point beim Aufruf von GetDrives getroffen wurde. Es geht also darum, in einen anderen GetDrives-Call zu gehen. – CodeNaked

+1

@Brad - Es gibt mehrere WM_DEVICECHANGE Nachrichten gesendet werden. Wenn WndProc das erste Mal aufgerufen wird, behandelt es die erste dieser Nachrichten. Der GetDrives-Aufruf pumpt die Nachrichten, um alle COM-Aufrufe in den STA-Thread zu mahnen (z. B. die Rückgabewerte der WMI-Objekte). Da weitere WM_DEVICECHANGE-Nachrichten auf die Verarbeitung warten, werden sie durch das Pumpen der Nachrichtenwarteschlange gezwungen, durch die WndProc-Überschreibung zu gehen. Also die Rekursion. – CodeNaked