Новости из Блогов Игрища с USB в Windows (отслеживаем и контролируем)

Discussion in 'Мировые новости. Обсуждения.' started by Suicide, 21 Jul 2012.

  1. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,482
    Likes Received:
    7,062
    Reputations:
    693
    Игрища с USB в Windows (отслеживаем и контролируем)


    [​IMG]

    Не так давно появилась задача - каким-то образом отслеживать появление новых USB-флешек в Windows, а также их исчезновение. В идеале также неплохо было бы иметь возможность манипулировать безопасным извлечением. Казалось бы, что все просто - ведь Windows имеет средства для оповещения приложений о вставляемых и вытаскиваемых флешках на уровне пользователя, но на деле оказалось, что тонкостей в этом вопросе очень много.

    Итак, в статье я расскажу, как:
    [+] отследить появление новой флешки или USB-диска в системе (даже если это хитрожопая флешка, которая монтируется как CD-ROM+Flash, например, или флешка, разбитая на пару дисков)
    [+] отслеживать безопасное извлечение флешек и манипулировать им
    [+] самому безопасно извлечь любой извлекаемый USB-девайс по букве его диска
    [+] отследить прочие события, а именно небезопасное извлечение флешки и отказ в безопасном извлечении

    Само-собой, никаких драйверов, только уровень пользователя! Я также поделюсь с вами исходником класса на C++, который реализует все вышеописанные задачи. Давно я не писал годных толстых статей...

    Надеюсь, вы приготовились погрузиться в недра WinAPI. Начнем с теории. В Windows существуют специальные события, которые система посылает, когда пользователь вставляет какой-либо USB-девайс в свой компьютер или вытаскивает его. Это DBT_DEVICEARRIVAL[/COLOR и DBT_DEVICEREMOVECOMPLETE. Оба эти ивента шлются через сообщение WM_DEVICECHANGE. Есть и другие полезные события, но о них позже. Казалось бы, все хорошо, и мы с помощью этих событий сразу имеем букву диска, на которую смонтировалось устройство (если это флешка или внешний жесткий диск).

    Но на самом деле все не так радужно. Представим такую ситуацию: пользователь вставляет в компьютер новомодную флешку, которая монтируется как CD-ROM, содержащий сопутствующее ПО для нее, и как непосредственно флеш-диск. Через DBT_DEVICEARRIVAL действительно придет сообщение о добавлении двух дисков в систему. Однако, если пользователь вытащит флешку с помощью функции безопасного извлечения устройств, мы узнаем через ивент DBT_DEVICEREMOVECOMPLETE только о том, что был извлечен один из этих двух дисков. Такая ситуация стабильно проявляется как минимум на Windows 7. Я нашел путь, который позволяет стопроцентно определить, какие устройства добавились или удалились из системы - при получении DBT_DEVICEARRIVAL или DBT_DEVICEREMOVECOMPLETE достаточно перечислить все установленные в системе диски, найти среди них USB-девайсы и сравнить полученный список с предыдущим состоянием до прихода события. Возможно, это не очень оптимально, зато мы точно узнаем, какие USB-устройства были примонтированы и удалены из системы.

    Что ж, переходим к более сложной части - работа с безопасным извлечением устройств. Представим, что мы читаем какой-то файл с флешки или пишем его. И тут пользователь запросил безопасное извлечение. Если мы тут же не закроем все хендлы и не завершим дисковые операции, система скажет пользователю, что диск занят. Как обработать эту ситуацию корректно? Казалось бы, все просто - есть же событие DBT_DEVICEQUERYREMOVE. Все верно, это то, что нам нужно. Только вот оно не отсылается системой по умолчанию, как я понял. Как быть? На просторах интернета было найдено решение: необходимо открыть замонтированный диск с помощью функции CreateFile (с флагом FILE_FLAG_BACKUP_SEMANTICS) и держать его открытым. Далее необходимо зарегистрировать оповещение о событии безопасного извлечения с помощью RegisterDeviceNotification с типом DBT_DEVTYP_HANDLE (регистрируем по хэндлу устройства, который получили из предыдущего вызова). После этого система начнет слать нам событие DBT_DEVICEQUERYREMOVE с типом (dbch_devicetype) DBT_DEVTYP_HANDLE. Теперь мы сможем определить, какое из подконтрольных нам устройств пользователь хочет безопасно извлечь, и даже вмешаться в этот процесс. Делается это достаточно просто - если мы не хотим позволять системе делать безопасное извлечение устройства, достаточно из обработчика DBT_DEVICEQUERYREMOVE вернуть значение BROADCAST_QUERY_DENY, а если хотим - то TRUE, не забыв при этом снять регистрацию ивента с помощью UnregisterDeviceNotification и закрыть с помощью CloseHandle хендл устройства. Во время обработки ивента DBT_DEVICEQUERYREMOVE мы можем по-быстрому закрыть все прочие хендлы, если наша программа в этот момент использует флешку, завершить все операции записи/чтения.

    Но тут есть еще одна тонкость - на эти операции система дает нам ограниченное время. Таймаут примерно 10-15 секунд (в Win 7 меньше, в XP больше). Если мы не успели вернуть из обработчика DBT_DEVICEQUERYREMOVE ничего, то система просто выдаст пользователю сообщение о том, что устройство занято и не может быть извлечено. Даже если мы вернем TRUE после этого, это ни на что не повлияет - устройство так и останется неизвлеченным. Однако, с этой проблемой тоже можно бороться. Ничто не мешает нам определить, прошел ли тамаут ожидания ответа на событие, и если прошел, то принудительно демонтировать устройство после того, как мы реально закончим с ним работать. Это, конечно, не спасет от назойливой таблички с сообщением о занятости флешки, но первоначальное желание пользователя будет выполнено, так как устройство в конечном счете будет извлечено. Разумеется, не стоит слишком много времени тратить на работу с флешкой после того, как пользователь пожелал ее извлечь, иначе он просто выдернет ее из компьютера, а это не то, чего мы хотим.

    Вы еще можете спросить, что произойдет, если пользователь захочет безопасно извлечь флешку, разбитую на два или более логических диска. Ответ прост: DBT_DEVICEQUERYREMOVE будет вызван два или более раз, для каждого раздела. Если мы не освободим хотя бы один, то и безопасное извлечение обломится.

    Основные моменты я рассмотрел, но есть еще один. Все вышеперечисленные события работают только для приложений, у которых есть окно верхнего уровня. Сделать такое окно для любого приложения, в общем-то, не проблема. Но что, если мы разрабатываем службу Windows? Здесь все просто - необходимо зарегистрировать оповещения о событиях устройств с помощью уже упомянутой функции RegisterDeviceNotification с флагом DEVICE_NOTIFY_SERVICE_HANDLE. В этом случае все ивенты мы сможем обрабатывать внутри своего ServiceCtrlHandler'а (SERVICE_CONTROL_DEVICEEVENT). Код для этого я не писал, но разобраться проблемы не будет, так как никаких отличий в начинке этого кода по сути нет.

    Теперь перейдем к самому коду. Если кратко: класс usb_monitor, который я написал в результате всех этих исследований, позволяет отслеживать установку-вытаскивание флешки, нормально работает с любыми флеш-дисками, позволяет манипулировать безопасным отключением устройств. Используется немного буста для реализации коллбеков. Класс еще можно совершенствовать и улучшать, но он уже хорошо справляется со своими задачами. Рассмотрим сначала интерфейс класса (usb_monitor.h):

    Необходимые инклюды:
    Code:
    #pragma once
    #include <memory>
    #include <stdexcept>
    #include <set>
    #include <map>
    #include <string>
    #include <Windows.h>
    #include <cfgmgr32.h>
    #include <boost/function.hpp>
    Класс исключений, которые могут быть брошены некоторыми функциями класса usb_monitor:
    c
    Code:
    lass usb_monitor_exception : public std::runtime_error
    {
    public:
        explicit usb_monitor_exception(const std::string& message);
    };
    Далее - описание открытого интерфейса главного класса usb_monitor. Это синглтон, поэтому у него закрытый конструктор.
    Code:
    class usb_monitor
    {
    public:
        //Создает единственный экземпляр класса usb_monitor
        //monitor_hard_drives - опция отслеживания внешних жестких дисков
        static usb_monitor* create(bool monitor_hard_drives = false);
     
        //Удалить экземпляр класса из памяти
        static void remove();
    Вызовите функцию create для создания единственного экземпляра класса usb_monitor. После создания он всегда существует в памяти, и получить его инстанс можно с помощью этой же самой функции create. Вызовите remove для полного удаления из памяти инстанса класса. Идем дальше - теперь коллбеки, которые вы можете установить:
    Code:
      //Добавляет коллбек, вызываемый при добавлении нового USB flash-диска
        //Формат коллбека: void(char letter), где letter - буква добавившегося диска
        template<typename Handler>
        void on_device_add(Handler handler)
        {
            on_device_added_ = handler;
        }
     
        //Добавляет коллбек, вызываемый при небезопасном извлечении USB flash-диска
        //Формат коллбека: void(char letter), где letter - буква извлеченного диска
        template<typename Handler>
        void on_device_remove(Handler handler)
        {
            on_device_removed_ = handler;
        }
     
        //Добавляет коллбек, вызываемый при безопасном извлечении USB flash-диска
        //Формат коллбека: bool(char letter), где letter - буква извлеченного диска
        //Верните из коллбека false, если не хотите извлекать устройство, или true в противном случае
        //Если из коллбека вы вернете false, будет вызван коллбек on_device_remove_fail
        template<typename Handler>
        void on_device_safe_remove(Handler handler)
        {
            on_device_safe_removed_ = handler;
        }
     
        //Добавляет коллбек, вызываемый при неудачном безопасном извлечении USB flash-диска
        //(таймаут или ктото другой запретил вытаскивать диск)
        //Формат коллбека: void(char letter), где letter - буква диска
        template<typename Handler>
        void on_device_remove_fail(Handler handler)
        {
            on_device_remove_failed_ = handler;
        }
     
        //Функции для удаления существующих коллбеков
        void on_device_add();
        void on_device_remove();
        void on_device_safe_remove();
        void on_device_remove_fail();
    
    Если тут что-то непонятно, я поясню дальше на примере использования класса.
    
        //Стартует отслеживание USB
        void start();
        //Останавливает отслеживание USB
        void stop();
        //Запущено ли отслеживание USB
        bool is_started() const;
    
    Здесь, думаю, все очевидно - после создания класса и назначения коллбеков надо вызвать функцию start.
    
        //Взять под контроль существующие USB-флешки
        //Если устройство уже было замонтировано, ничего не произойдет
        //Для каждого замонтированного устройства будет вызван коллбек on_device_add
        void mount_existing_devices();
     
        //Освободить все флешки, которые ранее были взяты под контроль
        //Коллбеки не вызывает
        void unmount_all_devices();
    Тут немного поясню. Функция mount_existing_devices необходима для того, чтобы класс смог взять под свой контроль все флешки, которые уже существовали в системе до его создания. Если этого не сделать, мы не сможем манипулировать ими и отслеживать их состояние, так как по умолчанию класс подхватывает только те устройства, которые были вставлены уже после его создания. С unmount_all_devices все ясно - эта функция просто освобождает все ранее взятые под контроль флешки, после чего класс более их не отслеживает и не занимает. Никакие коллбеки при этом не вызовутся.
    Code:
     //Безопасно извлечь какое-либо устройство
        //Коллбеков не вызывает
        void safe_eject(char letter);
    Эта функция позволяет произвести принудительное безопасное извлечение устройства по букве диска, как будто пользователь кликнул "Безопасное извлечение устройства". Если флешка была подконтрольна классу, он перед отключением ее освободит. Никакие коллбеки при этом вызваны не будут.
    Code:
    //Установить опцию - отключать ли безопасно USB-устройство даже в том случае,
        //если после запроса на отключение от Windows прошел таймаут ожидания ответа
        //от приложения
        //По умолчанию включено
        void set_safe_remove_on_timeout(bool remove);
     
        //Включена ли опция безопасного отключения после таймаута ожидания Windows
        bool is_set_safe_remove_on_timeout_on() const;
    Эти функции реализуют тот функционал, который я описал выше - принудительное безопасное извлечение после истечения таймаута ожидания ответа на ивент DBT_DEVICEQUERYREMOVE.

    Наконец, парочка вспомогательных функций:
    Code:
    //Получить буквы всех USB flash-дисков, имеющихся в системе в данный момент времени
        //Если include_usb_hard_drives == true, то в список попадут буквы внешних жестких дисков,
        //в противном случае - только флешки
        static std::set<wchar_t> get_flash_disks(bool include_usb_hard_drives);
     
        //Вспомогательная функция для консольных приложений
        //Написал ее чисто для теста, вы наверняка будете организовывать
        //цикл сообщений Windows как-то по-другому
        static void message_pump();
    Настройки отслеживания внешних жестких дисков:
    Code:
     //Включить опцию отслеживания внешних жестких дисков
        //По умолчанию включена опция отслеживания только USB-флешек
        void enable_usb_hard_drive_monitoring(bool enable);
     
        //Включена ли опция отслеживания внешних жестких дисков
        bool is_usb_hard_drive_monitoring_enabled() const;
    Деструктор:
    Code:
    //Деструктор
        ~usb_monitor();
    Теперь - немного о закрытых переменных и функциях класса:
    Code:
    private:
        //Закрытый конструктор, создает скрытое окно для отслеживания ивентов устройств
        explicit usb_monitor(bool monitor_hard_drives);
     
        //Запрещаем копирование класса
        usb_monitor(const usb_monitor&);
        usb_monitor& operator=(const usb_monitor&);
     
        //Переменные для хранения состояния и хендла окна
        //и настройки мониторинга внешних жестких дисков
        bool started_, safe_remove_on_timeout_, monitor_hard_drives_;
        HWND mon_hwnd_;
     
        //Список существующих в данный момент USB-флешек и дисков
        std::set<wchar_t> existing_usb_devices_;
     
        //Зарегистрированные подписки на ивенты для флешек
        //В мапе хранится: <хендл_устройства, <хендл_оповещения, буква_диска> >
        typedef std::map<size_t, std::pair<HDEVNOTIFY, wchar_t> > notifications;
        notifications existing_notifications_;
     
        //Процедура обработки сообщений вспомогательного окна
        static INT_PTR WINAPI WinProcCallback(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
     
        //Инстанс класса usb_monitor
        static std::auto_ptr<usb_monitor> instanse_;
     
        //Вспомогательные функции, которые будут описаны далее
        void detect_changed_devices();
        void mount_device(wchar_t letter);
        void unmount_device(wchar_t letter, bool call_unsafe_callback = true);
        BOOL devices_changed(WPARAM wParam, LPARAM lParam);
     
        //Вспомогательная структура для получения всякой информации об устройстве
        struct device_info
        {
            DEVINST dev_inst;
            GUID dev_class;
            long dev_number;
        };
     
        //Эта функция возвращает информацию об устройстве по букве диска
        static const device_info get_device_info(char letter);
     
        //Коллбеки пользователя
        boost::function<void(char)> on_device_added_;
        boost::function<void(char)> on_device_removed_;
        boost::function<bool(char)> on_device_safe_removed_;
        boost::function<void(char)> on_device_remove_failed_;
     
        //Имя класса регистрируемого окна, об этом далее
        static const std::wstring class_name;
    };
    Теперь я покажу пример использования класса, чтобы сложилось законченное впечатление о его возможностях. После этого я перейду к детальному описанию исходного кода класса, если это, конечно, кого-то заинтересует.
    Пример - простое консольное приложение, которое показывает, какие флешки и USB-диски уже вставлены в компьютер, какие пользователь вынул без запроса на безопасное извлечение, какие вставил во время работы программы. Также оно отслеживает, какие диски пользователь хочет безопасно извлечь.
    Code:
    //Инклюды
    #include <iostream>
    #include <sstream>
    #include <Windows.h>
    //Наш класс
    #include "usb_monitor.h"
     
    //Будет вызвано при добавлении нового диска в систему
    void device_added(char letter)
    {
        std::cout << "Added USB disk: " << letter << std::endl;
    }
     
    //Будет вызвано при небезопасном извлечении какого-либо диска
    void device_removed(char letter)
    {
        std::cout << "UNSAFE-removed USB disk: " << letter << std::endl;
    }
     
    //Будет вызвано при безопасном извлечении какого-либо диска
    bool device_safe_removed(char letter)
    {
        std::wstringstream ss;
        ss << L"Разрешить извлечь диск " << static_cast<wchar_t>(letter) << L":?";
        if(MessageBox(0, ss.str().c_str(), L"?", MB_ICONQUESTION | MB_YESNO) == IDYES)
        {
            std::cout << "Safe-removed USB disk: " << letter << std::endl;
            return true;
        }
        else
        {
            return false;
        }
    }
     
    //Будет вызвано при ошибке безопасного извлечении какого-либо диска
    //(таймаут или запрет извлечения)
    void device_remove_failed(char letter)
    {
        std::cout << "Failed to eject device: " << letter << std::endl;
    }
     
    int main()
    {
        //Создаем экземпляр класса
        usb_monitor* mon = usb_monitor::create();
     
        //Внешние жесткие диски будем тоже мониторить
        mon->enable_usb_hard_drive_monitoring(true);
     
        //Устанавливаем интересующие нас коллбеки
        //Для демонстрации я использую все
        mon->on_device_add(device_added);
        mon->on_device_remove(device_removed);
        mon->on_device_safe_remove(device_safe_removed);
        mon->on_device_remove_fail(device_remove_failed);
     
        //Определяем, какие флешки и usb-диски уже вставлены
        //и берем их под контроль
        mon->mount_existing_devices();
     
        //Начинаем отслеживать события устройств
        mon->start();
     
        //Отладочная функция, которая просто запускает цикл сообщений
        //Для консольного приложения, чтобы скрытое окно usb_monitor
        //могло получать сообщения
        usb_monitor::message_pump();
     
        return 0;
    }
    Данный пример будет мониторить все USB-диски и флешки в системе, позволит манипулировать безопасным извлечением. Работать будет на системах от Windows XP/2003 до Windows 7 (на восьмерке не проверял, но скорее всего тоже будет все нормально). Разумеется, класс usb_test нельзя будет использовать в службах Windows - потребуется небольшая переработка. Но в любых пользовательских приложениях - запросто.

    Внутри коллбеков класса старайтесь не выполнять длительных операций, так как это остановит очередь сообщений Windows. Перекидывайте долгую обработку, если таковая имеется, в отдельный поток.

    [​IMG]

    Далее я начну более детальное описание кода для тех, кому интересно, как это все реализовано и работает. Незаинтересованных прошу сразу в конец статьи за файлами.
    Code:
    //Инклюды
    #include <Windows.h>
    #include <Winioctl.h>
    #include <cfgmgr32.h>
    #include <SetupAPI.h>
    #include <Dbt.h>
    #include <string>
    #include <vector>
    #include <algorithm>
    #include <iterator>
    #include "usb_monitor.h"
     
    //Либа, в которой находятся некоторые необходимые нам функции
    #pragma comment(lib, "SetupAPI")
     
    //Класс исключений (конструктор), тут ничего сложного
    usb_monitor_exception::usb_monitor_exception(const std::string& message)
        :std::runtime_error(message)
    {}
    Далее - пара статических членов класса usb_monitor:
    Code:
    //Указатель, содержащий единственный инстанс класса
    //(напоминаю, что usb_monitor - синглтон)
    std::auto_ptr<usb_monitor> usb_monitor::instanse_;
    //Имя регистрируемого класса окна для отслеживания USB-девайсов
    const std::wstring usb_monitor::class_name(L"UsbMonWinClass");
    Начнем разбор основного функционала с конструктора:
    Code:
    usb_monitor::usb_monitor(bool monitor_hard_drives)
        :started_(false), //инициализируем переменные класса начальными значениями
        safe_remove_on_timeout_(true),
        monitor_hard_drives_(false),
        mon_hwnd_(NULL)
    {
        //Структура класса окна, регистрируемого нами
        WNDCLASSEXW wndClass = {0};
     
        //Заполняем ее
        wndClass.cbSize = sizeof(WNDCLASSEX);
        wndClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
        wndClass.hInstance = reinterpret_cast<HINSTANCE>(GetModuleHandleW(NULL));
        //Коллбек WinProcCallback будет рассмотрен далее
        wndClass.lpfnWndProc = reinterpret_cast<WNDPROC>(WinProcCallback);
        wndClass.cbClsExtra = 0;
        wndClass.cbWndExtra = 0;
        //Имя класса (UsbMonWinClass, хранится в статической переменной, упомянутой выше)
        wndClass.lpszClassName = class_name.c_str();
     
        //Регистрируем оконный класс
        if(!RegisterClassExW(&wndClass))
            throw usb_monitor_exception("Cannot register window class");
     
        //Получаем и сохраняем список всех существующих USB-девайсов
        existing_usb_devices_ = get_flash_disks(monitor_hard_drives_);
     
        //Непосредственно создаем окно, отслеживающее события устройств
        mon_hwnd_ = CreateWindowExW(WS_EX_CONTROLPARENT,
            class_name.c_str(),
            L"UsbMon",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT, 0,
            640, 480,
            NULL, NULL,
            GetModuleHandleW(NULL),
            NULL);
     
        //Если что-то пошло не так
        if(mon_hwnd_ == NULL)
        {
            UnregisterClassW(class_name.c_str(), GetModuleHandleW(NULL));
            throw usb_monitor_exception("Cannot create window");
        }
     
        //Это необходимо, чтобы из статической оконной процедуры (WinProcCallback)
        //найти экземпляр нашего класса
        SetWindowLongPtrW(mon_hwnd_, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
    }
     
    _________________________
  2. Suicide

    Suicide Super Moderator
    Staff Member

    Joined:
    24 Apr 2009
    Messages:
    2,482
    Likes Received:
    7,062
    Reputations:
    693
    На этом этапе мы имеем скрытое окно, готовое принимать сообщения, оповещающие о различных событиях устройств. Посмотрим на код оконной процедуры этого окна, чтобы понять, как это делается:
    Code:
    //Статическая процедура, в которую сыпятся все оконные сообщения
    INT_PTR WINAPI usb_monitor::WinProcCallback(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        switch(message)
        {
        //Если это то, что нам нужно
        case WM_DEVICECHANGE:
            {
                //Получаем указатель на экземпляр нашего класса, который сохранили выше
                //и вызываем функцию, которая все обрабатывает
                LONG_PTR data = GetWindowLongPtrW(hWnd, GWLP_USERDATA);
                if(data)
                    return reinterpret_cast<usb_monitor*>(data)->devices_changed(wParam, lParam);
            }
            return TRUE;
        }
     
        return TRUE;
    }
     
    //Функция, которая вызывается для обработки сообщения WM_DEVICECHANGE
    //(уже нестатическая)
    BOOL usb_monitor::devices_changed(WPARAM wParam, LPARAM lParam)
    {
        if(started_)
        {
            //Структура информации об ивенте
            PDEV_BROADCAST_HDR pHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(lParam);
            switch(wParam)
            {
            //Если вставили устройство
            case DBT_DEVICEARRIVAL:
                //И если это дисковое устройство, то проверим
                //изменения в буквах дисков, интересующих нас
                if(pHdr->dbch_devicetype == DBT_DEVTYP_VOLUME)
                    detect_changed_devices();
                break;
     
            //Если какое-то устройство не удалось безопасно извлечь
            case DBT_DEVICEQUERYREMOVEFAILED:
                //И это хендл
                if(pHdr->dbch_devicetype == DBT_DEVTYP_HANDLE)
                {
                    PDEV_BROADCAST_HANDLE info = reinterpret_cast<PDEV_BROADCAST_HANDLE>(pHdr);
     
                    //Проверим, наш ли это хендл, который мы открыли с помощью CreateFile
                    notifications::iterator it = existing_notifications_.find(reinterpret_cast<size_t>(info->dbch_handle));
                    if(it != existing_notifications_.end())
                    {
                        //И если это так, то дернем коллбек, оповещающий о неудаче
                        //безопасного извлечения, передав туда букву диска
                        if(!on_device_remove_failed_.empty())
                            on_device_remove_failed_(static_cast<char>((*it).second.second));
                    }
                }
                break;
     
            //Если пришел запрос на безопасное извлечение устройства
            case DBT_DEVICEQUERYREMOVE:
                //И это хендл
                if(pHdr->dbch_devicetype == DBT_DEVTYP_HANDLE)
                {
                    PDEV_BROADCAST_HANDLE info = reinterpret_cast<PDEV_BROADCAST_HANDLE>(pHdr);
     
                    //Проверим, наш ли это хендл, который мы открыли с помощью CreateFile
                    notifications::iterator it = existing_notifications_.find(reinterpret_cast<size_t>(info->dbch_handle));
                    if(it != existing_notifications_.end())
                    {
                        //И если это так, спросим через коллбек, можем ли мы разрешить
                        //извлекать это устройство
                        if(!on_device_safe_removed_.empty())
                        {
                            //Если нет - вернем системе код отказа
                            if(!on_device_safe_removed_(static_cast<char>((*it).second.second)))
                                return BROADCAST_QUERY_DENY;
     
                            //Пользователь мог вызвать safe_eject внутри on_device_safe_removed, поэтому
                            //проверим этот момент еще раз
                            it = existing_notifications_.find(reinterpret_cast<size_t>(info->dbch_handle));
                        }
     
                        //Если коллбек не был задан, или программа разрешила извлечение
                        //и при этом девайс не был извлечен принудительно
                        if(it != existing_notifications_.end())
                        {
                            //Выясним, а не прошел ли уже таймаут ожидания системой
                            //ответа на ивент DBT_DEVICEQUERYREMOVE
                            if(safe_remove_on_timeout_ && (InSendMessageEx(NULL) & ISMEX_REPLIED))
                            {
                                //Если прошел и задана опция извлечения после таймаута,
                                //принудительно извлечем устройство
                                try
                                {
                                    safe_eject(static_cast<char>((*it).second.second));
                                }
                                catch(const usb_monitor_exception&)
                                {
                                    //Ничего не делаем, так как устройство
                                    //может быть занято кем-то еще
                                }
                            }
                            else
                            {
                                //Если таймаут не вышел, то освобождаем устройство
                                //и разрешаем его извлечь (return TRUE в самом низу)
                                UnregisterDeviceNotification((*it).second.first);
                                CloseHandle(info->dbch_handle);
                                existing_notifications_.erase(it);
                            }
                        }
                    }
                }
                break;
     
            //Если какое-то устройство извлечено
            //(небезопасно, например)
            case DBT_DEVICEREMOVECOMPLETE:
                //И это дисковое устройство, проверим изменения в интересующих нас буквах дисков
                if(pHdr->dbch_devicetype == DBT_DEVTYP_VOLUME)
                    detect_changed_devices();
                break;
            }
        }
     
        return TRUE;
    }
    Вот я и рассказал, как работает основная следящая за событиями процедура. В карте existing_notifications_ содержится информация о подконтрольных классу usb_monitor устройствах: их хендлы, хендлы соответствующих им нотификаций и соответствующие буквы дисков. Неопределенным пока что моментом является функция detect_changed_devices, которая в коде выше используется дважды. Она определяет, какие с момента последнего ее вызова устройства были добавлены и удалены. Разберем ее код:
    Code:
    void usb_monitor::detect_changed_devices()
    {
        //Список вставленных и вытащенных с последнего вызова функции устройств
        std::vector<wchar_t> inserted, ejected;
        //Получаем текущий список интересующих нас устройств
        std::set<wchar_t> new_device_list = get_flash_disks(monitor_hard_drives_);
        //С помощью стандартного алгоритма определяем, какие буквы дисков добавились,
        //а какие были удалены
        std::set_difference(new_device_list.begin(), new_device_list.end(), existing_usb_devices_.begin(), existing_usb_devices_.end(), 
            std::back_inserter(inserted));
        std::set_difference(existing_usb_devices_.begin(), existing_usb_devices_.end(), new_device_list.begin(), new_device_list.end(),
            std::back_inserter(ejected));
     
        //Сохраняем новый список устройств
        existing_usb_devices_ = new_device_list;
     
        //Берем под контроль вставленные устройства
        for(std::vector<wchar_t>::const_iterator it = inserted.begin(); it != inserted.end(); ++it)
            mount_device(*it);
     
        //И отпускаем извлеченные (в этом месте те устройства, которые были извлечены
        //безопасно, уже освобождены нами)
        for(std::vector<wchar_t>::const_iterator it = ejected.begin(); it != ejected.end(); ++it)
            unmount_device(*it);
    }
    Относительно несложная функция. Однако, она вызывает еще несколько. Сначала разберем, что такое mount_device и unmount_device. Эти функции берут устройство под контроль класса usb_monitor и освобождают его, соответственно. Начнем с mount_device:
    Code:
    void usb_monitor::mount_device(wchar_t letter)
    {
        //Проверяем, не подконтрольно ли нам уже это устройство
        for(notifications::iterator handle_it = existing_notifications_.begin(); handle_it != existing_notifications_.end(); ++handle_it)
            if((*handle_it).second.second == letter)
                return;
     
        //Формируем строку вида "X:", где, X - буква интересующего диска
        wchar_t drive_name[3] = {0};
        drive_name[0] = letter;
        drive_name[1] = L':';
        //Отключаем стандартный вывод ошибок в мессаджбоксах
        //Это необходимо для того, если мы наткнемся на отсутствующий диск, имеющий
        //тем не менее букву (кардридер без вставленной карты, например)
        UINT old_mode = SetErrorMode(SEM_FAILCRITICALERRORS);
        //Открываем устройство с флагом FILE_FLAG_BACKUP_SEMANTICS
        HANDLE device_handle = CreateFileW(
            drive_name,
            GENERIC_READ,
            FILE_SHARE_READ | FILE_SHARE_DELETE | FILE_SHARE_WRITE,
            NULL,
            OPEN_EXISTING,
            FILE_FLAG_BACKUP_SEMANTICS,
            NULL);
     
        //Возвращаем уровень ошибок на прежний
        SetErrorMode(old_mode);
     
        //Если какая-нибудь ошибка - выходим из функции
        if(device_handle == INVALID_HANDLE_VALUE)
            return;
     
        //Готовимся настроить уведомления
        DEV_BROADCAST_HANDLE NotificationFilter = {0};
        NotificationFilter.dbch_size = sizeof(DEV_BROADCAST_HANDLE);
        NotificationFilter.dbch_devicetype = DBT_DEVTYP_HANDLE;
        NotificationFilter.dbch_handle = device_handle;
     
        //Регистрируем оповещения о событиях с хендлом устройства
        //Последний флаг (DEVICE_NOTIFY_WINDOW_HANDLE) говорит о том, что
        //сообщения будут приходить нам в оконную процедуру
        HDEVNOTIFY token = RegisterDeviceNotificationW(mon_hwnd_, &NotificationFilter, DEVICE_NOTIFY_WINDOW_HANDLE);
        if(!token)
        {
            //Если ошибка - закрываем хендл и выходим из функции
            CloseHandle(device_handle);
            return;
        }
     
        //Запоминаем созданный хендл вместе с нотификацией и буквой диска
        existing_notifications_.insert(std::make_pair(reinterpret_cast<size_t>(device_handle), std::make_pair(token, letter)));
     
        //Если задан пользовательский коллбек, дернем его
        //и сообщим о том, что добавлен новый девайс
        if(!on_device_added_.empty())
            on_device_added_(static_cast<char>(letter));
    }
    С unmount_device все проще. Первый параметр - буква диска, а второй сообщает, следует ли вызывать коллбек пользователя, чтобы сообщить о том, что устройство извлечено небезопасно.
    Code:
    void usb_monitor::unmount_device(wchar_t letter, bool call_unsafe_callback)
    {
        //Проверяем, подконтролен ли нам девайс
        for(notifications::iterator handle_it = existing_notifications_.begin(); handle_it != existing_notifications_.end(); ++handle_it)
        {
            //Если да
            if((*handle_it).second.second == letter)
            {
                //Снимаем регистрацию событий
                UnregisterDeviceNotification((*handle_it).second.first);
                //Закрываем хендл устройства            
                CloseHandle(reinterpret_cast<HANDLE>((*handle_it).first));
                //Удаляем информацию об устройстве
                existing_notifications_.erase(handle_it);
     
                //Если надо - дергаем коллбек о небезопасном извлечении
                if(call_unsafe_callback && !on_device_removed_.empty())
                    on_device_removed_(static_cast<char>(letter));
     
                break;
            }
        }
    }
    Теперь - еще две коротенькие вспомогательные функции:
    Code:
    //Освободить все взятые под контроль устройства
    void usb_monitor::unmount_all_devices()
    {
        //Получаем список всех устройств
        std::set<wchar_t> devices(get_flash_disks(monitor_hard_drives_));
        //Освобождаем найденные устройства. Если устройство не под контролем, то
        //unmount_device просто ничего не сделает
        for(std::set<wchar_t>::const_iterator it = devices.begin(); it != devices.end(); ++it)
            unmount_device(*it, false);
    }
     
    //Взять под контроль все устройства
    void usb_monitor::mount_existing_devices()
    {
        //Здесь все аналогично предыдущей функции
        std::set<wchar_t> devices(get_flash_disks(monitor_hard_drives_));
        for(std::set<wchar_t>::const_iterator it = devices.begin(); it != devices.end(); ++it)
            mount_device(*it);
    }
    Вы, наверное, заметили, что во многих местах тут используется функция get_flash_disks. Смысл ее понятен, осталось разобрать содержание.
    Code:
    //Параметр include_usb_hard_drives говорит о том, стоит ли включать в список
    std::set<wchar_t> usb_monitor::get_flash_disks(bool include_usb_hard_drives)
    {
        std::set<wchar_t> devices;
     
        //Получаем список логических разделов
        unsigned int disks = GetLogicalDrives();
     
        //Строка для формирования имен вида A:, B:, ...
        wchar_t drive_root[] = L"?:";
     
        //Смотрим, какие логические разделы есть в системе
        for(int i = 31; i >= 0; i--)
        {
            //Если диск есть
            if(disks & (1 << i))
            {
                //Формируем строку с именем диска
                drive_root[0] = static_cast<wchar_t>('A') + i;
                //Получаем тип устройства
                DWORD type = GetDriveTypeW(drive_root);
     
                //Если это съемный девайс (флешка или флоппи)
                if(type == DRIVE_REMOVABLE)
                {
                    //Получаем тип девайса - это, похоже, самый простой
                    //путь отличить флешку от флоппика
                    wchar_t buf[MAX_PATH];
                    if(QueryDosDeviceW(drive_root, buf, MAX_PATH))
                        if(std::wstring(buf).find(L"\\Floppy") == std::wstring::npos) //Если в имени нет подстроки "\\Floppy",
                            devices.insert(static_cast<wchar_t>('A') + i); //то это флешка
                }
                //Если это какой-то жесткий диск, и мы их тоже мониторим
                else if(type == DRIVE_FIXED && include_usb_hard_drives)
                {
                    try
                    {
                        //Получаем информацию о девайсе
                        device_info info(get_device_info('A' + i));
                        //Получаем хендл к набору различных сведений о классе устройств info.dev_class на локальном компьютере
                        //Подробнее читайте MSDN, а функция get_device_info описана ниже
                        HDEVINFO dev_info = SetupDiGetClassDevsW(&info.dev_class, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
     
                        //Если хендл получен
                        if(dev_info != INVALID_HANDLE_VALUE)
                        {
                            SP_DEVINFO_DATA dev_data;
                            dev_data.cbSize = sizeof(dev_data);
                            //Получаем информацию о жестком диске
                            if(SetupDiEnumDeviceInfo(dev_info, info.dev_number, &dev_data))
                            {
                                DWORD properties;
                                //Получаем информацию о свойстве SPDRP_REMOVAL_POLICY жесткого диска
                                //Оно говорит о том, может ли устройство быть извлечено
                                //Если может, добавим его в результирующий набор
                                if(SetupDiGetDeviceRegistryPropertyW(dev_info, &dev_data, SPDRP_REMOVAL_POLICY, NULL, reinterpret_cast<PBYTE>(&properties), sizeof(properties), NULL)
                                    &&
                                    properties != CM_REMOVAL_POLICY_EXPECT_NO_REMOVAL)
                                    devices.insert(static_cast<wchar_t>('A') + i);
                            }
     
                            //Освободим информационный хендл
                            SetupDiDestroyDeviceInfoList(dev_info);
                        }
                    }
                    catch(const usb_monitor_exception&)
                    {
                        //Ошибки тут игнорируем
                    }
                }
            }
        }
     
        //Возвращаем набор
        return devices;
    }
    Поиск устройств можно было бы и оптимизировать, но он производится не так часто, поэтому такая реализация вполне адекватна. Не очень просто отличается флоппи-дисковод от флешки или карты памяти - приходится пользоваться парой функций. Кроме того, отличить внешний съемный жесткий диск от встроенного также не очень тривиально: приходится использовать Setup-функции для получения информации о свойствах устройства.

    Последним штрихом остается нерассмотренная функция get_device_info. Этот код был стянут с какого-то сайта, кажется, codeproject, и немного переписан.
    Code:
    //Структура device_info описана выше
    const usb_monitor::device_info usb_monitor::get_device_info(char letter)
    {
        //Формируем строку вида \\.\X: для устройства
        wchar_t volume_access_path[] = L"\\\\.\\X:";
        volume_access_path[4] = static_cast<wchar_t>(letter);
     
        //Открываем его
        HANDLE vol = CreateFileW(volume_access_path, 0,
            FILE_SHARE_READ | FILE_SHARE_WRITE,
            NULL, OPEN_EXISTING, 0, NULL);
     
        //Если ошибка - бросим исключение
        if(vol == INVALID_HANDLE_VALUE)
            throw usb_monitor_exception("Cannot open device");
     
        //Теперь надо получить номер устройства
        STORAGE_DEVICE_NUMBER sdn;
        DWORD bytes_ret = 0;
        long DeviceNumber = -1;
     
        //Это делается таким IOCTL-запросом к устройству
        if(DeviceIoControl(vol,
            IOCTL_STORAGE_GET_DEVICE_NUMBER,
            NULL, 0, &sdn, sizeof(sdn),
            &bytes_ret, NULL))
            DeviceNumber = sdn.DeviceNumber;
     
        //Хендл нам больше не нужен
        CloseHandle(vol);
     
        //Если номер не получен - ошибка
        if(DeviceNumber == -1)
            throw usb_monitor_exception("Cannot get device number");
     
        //Еще две вспомогательные строки вида X: и X:\
        wchar_t devname[] = L"?:";
        wchar_t devpath[] = L"?:\\";
        devname[0] = static_cast<wchar_t>(letter);
        devpath[0] = static_cast<wchar_t>(letter);
        wchar_t dos_name[MAX_PATH + 1];
        //Этот момент уже описан выше - используется для определения
        //флешек и флопиков
        if(!QueryDosDeviceW(devname, dos_name, MAX_PATH))
            throw usb_monitor_exception("Cannot get device info");
     
        bool floppy = std::wstring(dos_name).find(L"\\Floppy") != std::wstring::npos;
        //Определяем тип устройства
        UINT drive_type = GetDriveTypeW(devpath);
     
        const GUID* guid;
     
        //Теперь выясним класс устройства, с которым имеем дело
        switch(drive_type)
        {
        case DRIVE_REMOVABLE:
            if(floppy)
                guid = &GUID_DEVINTERFACE_FLOPPY; //флоппи
            else
                guid = &GUID_DEVINTERFACE_DISK; //какой-то диск
            break;
     
        case DRIVE_FIXED:
            guid = &GUID_DEVINTERFACE_DISK; //какой-то диск
            break;
     
        case DRIVE_CDROM:
            guid = &GUID_DEVINTERFACE_CDROM; //CD-ROM
            break;
     
        default:
            throw usb_monitor_exception("Unknown device"); //Неизвестный тип
        }
     
        //Получаем хендл к набору различных сведений о классе устройств info.dev_class на локальном компьютере,
        //выше эта функция уже была упомянута
        HDEVINFO dev_info = SetupDiGetClassDevsW(guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
     
        //Если что-то не так, кинем исключение
        if(dev_info == INVALID_HANDLE_VALUE)
            throw usb_monitor_exception("Cannot get device class");
     
        DWORD index = 0;
        BOOL ret = FALSE;
     
        BYTE buf[1024];
        PSP_DEVICE_INTERFACE_DETAIL_DATA_W pspdidd = reinterpret_cast<PSP_DEVICE_INTERFACE_DETAIL_DATA_W>(buf);
        SP_DEVICE_INTERFACE_DATA spdid;
        SP_DEVINFO_DATA spdd;
        DWORD size;
     
        spdid.cbSize = sizeof(spdid);
     
        bool found = false;
     
        //Готовимся найти наш девайс через перечисление
        while(true)
        {
            //Перечисляем все устройства заданного класса
            ret = SetupDiEnumDeviceInterfaces(dev_info, NULL, guid, index, &spdid);
            if(!ret)
                break;
     
            //Получаем размер данных об устройстве
            size = 0;
            SetupDiGetDeviceInterfaceDetailW(dev_info, &spdid, NULL, 0, &size, NULL);
     
            if(size != 0 && size <= sizeof(buf))
            {
                pspdidd->cbSize = sizeof(*pspdidd);
     
                ZeroMemory(reinterpret_cast<PVOID>(&spdd), sizeof(spdd));
                spdd.cbSize = sizeof(spdd);
     
                //А теперь получаем информацию об устройстве
                BOOL res = SetupDiGetDeviceInterfaceDetailW(dev_info, &spdid, pspdidd, size, &size, &spdd);
                if(res)
                {
                    //Если все окей, открываем девайс по пути, который узнали
                    HANDLE drive = CreateFileW(pspdidd->DevicePath, 0,
                        FILE_SHARE_READ | FILE_SHARE_WRITE,
                        NULL, OPEN_EXISTING, 0, NULL);
                    if(drive != INVALID_HANDLE_VALUE)
                    {
                        //Получаем номер устройства, и если он совпадает
                        //с определенным нами ранее,
                        //то нужное устройство мы нашли
                        STORAGE_DEVICE_NUMBER sdn;
                        DWORD bytes_returned = 0;
                        if(DeviceIoControl(drive,
                            IOCTL_STORAGE_GET_DEVICE_NUMBER,
                            NULL, 0, &sdn, sizeof(sdn),
                            &bytes_returned, NULL))
                        {
                            if(DeviceNumber == static_cast<long>(sdn.DeviceNumber))
                            {
                                //Если нашли, то выходим из цикла
                                CloseHandle(drive);
                                found = true;
                                break;
                            }
                        }
     
                        CloseHandle(drive);
                    }
                }
            }
            index++;
        }
     
        SetupDiDestroyDeviceInfoList(dev_info);
     
        //А если не нашли устройство - то кинем эксепшен
        if(!found)
            throw usb_monitor_exception("Cannot find device");
     
        //Находим родителя устройства
        //Например, USB-хаб для флешки
        DEVINST dev_parent = 0;
        if(CR_SUCCESS != CM_Get_Parent(&dev_parent, spdd.DevInst, 0))
            throw usb_monitor_exception("Cannot get device parent");
     
        //Заполняем нашу структуру всякой интересной
        //информацией об устройстве
        device_info info;
        info.dev_class = *guid;
        info.dev_inst = dev_parent;
        info.dev_number = DeviceNumber;
     
        //И возвращаем ее
        return info;
    }
    Основная самая умная часть кода завершилась. Остались мелочи, некоторые из которых я тем не менее опишу. Для начала, функция безопасного извлечения устройства по букве его диска.
    Code:
    void usb_monitor::safe_eject(char letter)
    {
        //Хендл экземпляра девайса для локальной машины 
        DEVINST dev = get_device_info(letter).dev_inst;
     
        //Проверим, не подконтролен ли нам этот девайс, и если это так, освободим его
        unmount_device(static_cast<wchar_t>(letter), false);
     
        //Вызываем функцию безопасного извлечения. 2-5 параметры не передаем, чтобы
        //проводник смог сообщить пользователю о том, что смог/не смог извлечь устройство
        if(CR_SUCCESS != CM_Request_Device_EjectW(dev, NULL, NULL, 0, 0))
            throw usb_monitor_exception("Cannot safe-eject device");
    }
    Про непонятный тип DEVINST можно подробнее прочитать здесь.

    Можно для полноты картины рассмотреть деструктор класса usb_monitor:
    Code:
    usb_monitor::~usb_monitor()
    {
        started_ = false;
     
        //Удаляем отслеживающее события устройств окно
        SetWindowLongPtrW(mon_hwnd_, GWLP_USERDATA, 0);
        DestroyWindow(mon_hwnd_);
        //Снимаем регистрацию его класса
        UnregisterClassW(class_name.c_str(), GetModuleHandleW(NULL));
     
        //Освобождаем все устройства
        for(notifications::iterator handle_it = existing_notifications_.begin(); handle_it != existing_notifications_.end(); ++handle_it)
        {
            UnregisterDeviceNotification((*handle_it).second.first);
            CloseHandle(reinterpret_cast<HANDLE>((*handle_it).first));
        }
    }
    Оставшиеся функции совсем очевидны и неинтересны, их вы найдете в полном исходном коде класса. Надеюсь, тема работы с USB-устройствами немного приоткрылась для вас. До новых встреч и удачи в кодинге!

    Скачать полный исходный код класса (почти без комментариев) и код примера работы с ним: ZIP


    19. Июль 2012
    автор: dx
    http://kaimi.ru/2012/07/windows-usb-monitoring/
    http://kaimi.ru/
     
    _________________________
Loading...