DisconnectedContext MDA при вызове функций WMI в однопоточном приложении

Я пишу приложение в С#,.NET 3.0 в VS2005 с функцией мониторинга вставки/извлечения различных съемных дисков (USB-флеш-дисков, компакт-дисков и т.д.). Я не хотел использовать WMI, поскольку это может быть иногда неоднозначным (например, он может порождать несколько событий вставки для одного USB-накопителя), поэтому я просто переопределяю WndProc моей основной формы, чтобы поймать сообщение WM_DEVICECHANGE, как предлагалось здесь. Вчера я столкнулся с проблемой, когда выяснилось, что мне придется использовать WMI в любом случае, чтобы получить некоторые неясные данные о диске, такие как серийный номер. Оказывается, что вызов подпрограмм WMI из WndProc вызывает DisconnectedContext MDA.

После некоторого рытья я закончил неловкое обходное решение для этого. Код выглядит следующим образом:

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

что в основном означает запуск процедуры, связанной с WMI, в отдельном потоке, но затем, ожидая ее завершения.

Теперь вопрос: почему работает, а почему он должен быть таким? (или, не так ли?)

Я не понимаю факт получения DisconnectedContext MDA или RPC_E_WRONG_THREAD в первую очередь. Как работает процедура GetDrives() от обработчика события нажатия кнопки отличается от вызова его из WndProc? Разве они не происходят в одной и той же основной теме моего приложения? BTW, мое приложение полностью однопоточное, так почему же внезапно возникает ошибка со ссылкой на какой-то "неправильный поток"? Использует ли WMI многопоточность и специальную обработку функций из System.Management?

В то же время я нашел еще один вопрос, связанный с этим MDA, здесь . Хорошо, я могу считать, что вызов WMI означает создание отдельного потока для базового COM-компонента, но мне все еще не приходит в голову, почему нет необходимости в магии при вызове его после нажатия кнопки, и требуется магия при вызове это из WndProc.

Я действительно запутался в этом и был бы признателен за некоторые разъяснения по этому вопросу. Есть только несколько худших вещей, кроме решения и не зная, почему это работает:/

Cheers, Александр

Ответ 1

Существует довольно продолжительное обсуждение COM-квартир и сообщений о перекачке здесь. Но основной интерес представляет собой насос сообщений, используемый для обеспечения правильной сортировки вызовов в STA. Поскольку поток пользовательского интерфейса является рассматриваемой STA, сообщения нужно закачивать, чтобы обеспечить правильное функционирование.

Сообщение WM_DEVICECHANGE может быть отправлено в окно несколько раз. Поэтому в случае, когда вы вызываете GetDrives напрямую, вы фактически получаете рекурсивные вызовы. Поместите точку останова на вызов GetDrives, а затем прикрепите устройство для запуска события.

В первый раз, когда вы попали в точку останова, все в порядке. Теперь нажмите F5, чтобы продолжить, и вы снова попадете в точку останова. На этот раз стек вызовов выглядит примерно так:

[В режиме ожидания, ожидания или соединения]     УдалитьMeWindowsForms.exe! УдалитьMeWindowsForms.Form1.WndProc(ref System.Windows.Forms.Message m) Строка 46 С#     System.Windows.Forms.dll! System.Windows.Forms.Control.ControlNativeWindow.OnMessage(ref System.Windows.Forms.Message m) + 0x13 байт
    System.Windows.Forms.dll! System.Windows.Forms.Control.ControlNativeWindow.WndProc(ref System.Windows.Forms.Message m) + 0x31 байт
    System.Windows.Forms.dll! System.Windows.Forms.NativeWindow.DebuggableCallback(System.IntPtr hWnd, int msg, System.IntPtr wparam, System.IntPtr lparam) + 0x64 байт     Переход к управляемому переходному процессу

    [Управляемый для коренного перехода]
    mscorlib.dll! System.Threading.WaitHandle.InternalWaitOne(System.Runtime.InteropServices.SafeHandle waitableSafeHandle, long millisecondsTimeout, bool hasThreadAffinity, bool exitContext) + 0x2b байт     mscorlib.dll! System.Threading.WaitHandle.WaitOne(int millisecondsTimeout, bool exitContext) + 0x2d байт
    mscorlib.dll! System.Threading.WaitHandle.WaitOne() + 0x10 байт     System.Management.dll! System.Management.MTAHelper.CreateInMTA(тип System.Type) + 0x17b байт
    System.Management.dll! System.Management.ManagementPath.CreateWbemPath(строка) + 0x18 байт     System.Management.dll! System.Management.ManagementClass.ManagementClass(строка) + 0x29 байт
    УдалитьMeWindowsForms.exe! УдалитьMeWindowsForms.Form1.GetDrives() Строка 23 + 0x1b байтов С#

Таким образом, сообщения окна накачиваются, чтобы гарантировать, что COM-вызовы должным образом распределены, но это имеет побочный эффект снова вызвать WndProc и GetDrives (поскольку есть ожидающие сообщения WM_DEVICECHANGE), но все же в предыдущем вызове GetDrives. Когда вы используете BeginInvoke, вы удаляете этот рекурсивный вызов.

Опять положите точку останова на вызов GetDrives и нажмите F5 после первого удара. В следующий раз, подождите секунду или два, а затем снова нажмите F5. Иногда это терпит неудачу, иногда это не так, и вы снова попадете в точку останова. На этот раз ваш столбец будет включать три вызова в GetDrives, причем последний из них инициируется перечислением коллекции diskDriveList. Потому что снова сообщения перекачиваются, чтобы гарантировать, что вызовы маршалируются.

Трудно точно определить, почему срабатывает MDA, но при рекурсивных вызовах разумно предположить, что контекст COM может быть разорван преждевременно и/или объект собирается до того, как освобожденный объект COM может быть освобожден.