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

HTTP/1.1 200 OK Server: nginx Date: Sat, 08 Sep 2012 10:19:10 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Vary: Cookie X-Pingback: http://kaimi.ru/xmlrpc.php Link: ; rel=shortlink Игрища с USB в Windows (отслеживаем и контролируем)

 

Print This Post Игрища с USB в Windows (отслеживаем и контролируем)

Четверг, 19. Июль 2012
Раздел: C/C++, Windows, Для новичков, Сниппеты, автор: dx

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

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

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

Надеюсь, вы приготовились погрузиться в недра WinAPI. Начнем с теории. В Windows существуют специальные события, которые система посылает, когда пользователь вставляет какой-либо USB-девайс в свой компьютер или вытаскивает его. Это DBT_DEVICEARRIVAL и 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):

Необходимые инклюды:

#pragma once#include <memory>#include <stdexcept>#include <set>#include <map>#include <string>#include <Windows.h>#include <cfgmgr32.h>#include <boost/function.hpp>

Класс исключений, которые могут быть брошены некоторыми функциями класса usb_monitor:

class usb_monitor_exception :public std::runtime_error{public:explicit usb_monitor_exception(const std::string& message);};

Далее - описание открытого интерфейса главного класса usb_monitor. Это синглтон, поэтому у него закрытый конструктор.

class usb_monitor
{public://Создает единственный экземпляр класса usb_monitor//monitor_hard_drives - опция отслеживания внешних жестких дисковstatic usb_monitor* create(bool monitor_hard_drives =false);
 
    //Удалить экземпляр класса из памятиstaticvoidremove();

Вызовите функцию create для создания единственного экземпляра класса usb_monitor. После создания он всегда существует в памяти, и получить его инстанс можно с помощью этой же самой функции create. Вызовите remove для полного удаления из памяти инстанса класса. Идем дальше - теперь коллбеки, которые вы можете установить:

//Добавляет коллбек, вызываемый при добавлении нового 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_failtemplate<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();

Если тут что-то непонятно, я поясню дальше на примере использования класса.

//Стартует отслеживание USBvoid start();//Останавливает отслеживание USBvoid stop();//Запущено ли отслеживание USBbool is_started()const;

Здесь, думаю, все очевидно - после создания класса и назначения коллбеков надо вызвать функцию start.

//Взять под контроль существующие USB-флешки//Если устройство уже было замонтировано, ничего не произойдет//Для каждого замонтированного устройства будет вызван коллбек on_device_addvoid mount_existing_devices();
 
    //Освободить все флешки, которые ранее были взяты под контроль//Коллбеки не вызываетvoid unmount_all_devices();

Тут немного поясню. Функция mount_existing_devices необходима для того, чтобы класс смог взять под свой контроль все флешки, которые уже существовали в системе до его создания. Если этого не сделать, мы не сможем манипулировать ими и отслеживать их состояние, так как по умолчанию класс подхватывает только те устройства, которые были вставлены уже после его создания. С unmount_all_devices все ясно - эта функция просто освобождает все ранее взятые под контроль флешки, после чего класс более их не отслеживает и не занимает. Никакие коллбеки при этом не вызовутся.

//Безопасно извлечь какое-либо устройство//Коллбеков не вызываетvoid safe_eject(char letter);

Эта функция позволяет произвести принудительное безопасное извлечение устройства по букве диска, как будто пользователь кликнул "Безопасное извлечение устройства". Если флешка была подконтрольна классу, он перед отключением ее освободит. Никакие коллбеки при этом вызваны не будут.

//Установить опцию - отключать ли безопасно USB-устройство даже в том случае,//если после запроса на отключение от Windows прошел таймаут ожидания ответа//от приложения//По умолчанию включеноvoid set_safe_remove_on_timeout(boolremove);
 
    //Включена ли опция безопасного отключения после таймаута ожидания Windowsbool is_set_safe_remove_on_timeout_on()const;

Эти функции реализуют тот функционал, который я описал выше - принудительное безопасное извлечение после истечения таймаута ожидания ответа на ивент DBT_DEVICEQUERYREMOVE.

Наконец, парочка вспомогательных функций:

//Получить буквы всех USB flash-дисков, имеющихся в системе в данный момент времени//Если include_usb_hard_drives == true, то в список попадут буквы внешних жестких дисков,//в противном случае - только флешкиstatic std::set<wchar_t> get_flash_disks(bool include_usb_hard_drives);
 
    //Вспомогательная функция для консольных приложений//Написал ее чисто для теста, вы наверняка будете организовывать//цикл сообщений Windows как-то по-другомуstaticvoid message_pump();

Настройки отслеживания внешних жестких дисков:

//Включить опцию отслеживания внешних жестких дисков//По умолчанию включена опция отслеживания только USB-флешекvoid enable_usb_hard_drive_monitoring(bool enable);
 
    //Включена ли опция отслеживания внешних жестких дисковbool is_usb_hard_drive_monitoring_enabled()const;

Деструктор:

//Деструктор
    ~usb_monitor();

Теперь - немного о закрытых переменных и функциях класса:

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_monitorstatic 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;};
 
    //Эта функция возвращает информацию об устройстве по букве дискаstaticconst 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_;
 
    //Имя класса регистрируемого окна, об этом далееstaticconst std::wstring class_name;};

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

//Инклюды#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;returntrue;}else{returnfalse;}}
 
//Будет вызвано при ошибке безопасного извлечении какого-либо диска//(таймаут или запрет извлечения)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();
 
    return0;}

Данный пример будет мониторить все USB-диски и флешки в системе, позволит манипулировать безопасным извлечением. Работать будет на системах от Windows XP/2003 до Windows 7 (на восьмерке не проверял, но скорее всего тоже будет все нормально). Разумеется, класс usb_test нельзя будет использовать в службах Windows - потребуется небольшая переработка. Но в любых пользовательских приложениях - запросто.

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

Далее я начну более детальное описание кода для тех, кому интересно, как это все реализовано и работает. Незаинтересованных прошу сразу в конец статьи за файлами.

//Инклюды#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:

//Указатель, содержащий единственный инстанс класса//(напоминаю, что usb_monitor - синглтон)
std::auto_ptr<usb_monitor> usb_monitor::instanse_;//Имя регистрируемого класса окна для отслеживания USB-девайсовconst std::wstring usb_monitor::class_name(L"UsbMonWinClass");

Начнем разбор основного функционала с конструктора:

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));}

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

//Статическая процедура, в которую сыпятся все оконные сообщения
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)returnreinterpret_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_DEVICEQUERYREMOVEif(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, которая в коде выше используется дважды. Она определяет, какие с момента последнего ее вызова устройства были добавлены и удалены. Разберем ее код:

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:

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 все проще. Первый параметр - буква диска, а второй сообщает, следует ли вызывать коллбек пользователя, чтобы сообщить о том, что устройство извлечено небезопасно.

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;}}}

Теперь - еще две коротенькие вспомогательные функции:

//Освободить все взятые под контроль устройства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. Смысл ее понятен, осталось разобрать содержание.

//Параметр include_usb_hard_drives говорит о том, стоит ли включать в список
std::set<wchar_t> usb_monitor::get_flash_disks(bool include_usb_hard_drives){
    std::set<wchar_t> devices;
 
    //Получаем список логических разделовunsignedint 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);//то это флешка}//Если это какой-то жесткий диск, и мы их тоже мониторимelseif(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, и немного переписан.

//Структура 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-ROMbreak;
 
    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;}

Основная самая умная часть кода завершилась. Остались мелочи, некоторые из которых я тем не менее опишу. Для начала, функция безопасного извлечения устройства по букве его диска.

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:

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

Также рекомендую почитать

 Обсудить на форуме



Оставьте ваш комментарий

 

 

HTTP/1.1 200 OK Server: nginx Date: Sat, 08 Sep 2012 10:19:10 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Vary: Cookie X-Pingback: http://kaimi.ru/xmlrpc.php Link: ; rel=shortlink Игрища с USB в Windows (отслеживаем и контролируем)

 

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

Четверг, 19. Июль 2012
Раздел: C/C++, Windows, Для новичков, Сниппеты, автор: dx

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

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

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

Надеюсь, вы приготовились погрузиться в недра WinAPI. Начнем с теории. В Windows существуют специальные события, которые система посылает, когда пользователь вставляет какой-либо USB-девайс в свой компьютер или вытаскивает его. Это DBT_DEVICEARRIVAL и 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):

Необходимые инклюды:

#pragma once#include <memory>#include <stdexcept>#include <set>#include <map>#include <string>#include <Windows.h>#include <cfgmgr32.h>#include <boost/function.hpp>

Класс исключений, которые могут быть брошены некоторыми функциями класса usb_monitor:

class usb_monitor_exception :public std::runtime_error{public:explicit usb_monitor_exception(const std::string& message);};

Далее - описание открытого интерфейса главного класса usb_monitor. Это синглтон, поэтому у него закрытый конструктор.

class usb_monitor
{public://Создает единственный экземпляр класса usb_monitor//monitor_hard_drives - опция отслеживания внешних жестких дисковstatic usb_monitor* create(bool monitor_hard_drives =false);
 
    //Удалить экземпляр класса из памятиstaticvoidremove();

Вызовите функцию create для создания единственного экземпляра класса usb_monitor. После создания он всегда существует в памяти, и получить его инстанс можно с помощью этой же самой функции create. Вызовите remove для полного удаления из памяти инстанса класса. Идем дальше - теперь коллбеки, которые вы можете установить:

//Добавляет коллбек, вызываемый при добавлении нового 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_failtemplate<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();

Если тут что-то непонятно, я поясню дальше на примере использования класса.

//Стартует отслеживание USBvoid start();//Останавливает отслеживание USBvoid stop();//Запущено ли отслеживание USBbool is_started()const;

Здесь, думаю, все очевидно - после создания класса и назначения коллбеков надо вызвать функцию start.

//Взять под контроль существующие USB-флешки//Если устройство уже было замонтировано, ничего не произойдет//Для каждого замонтированного устройства будет вызван коллбек on_device_addvoid mount_existing_devices();
 
    //Освободить все флешки, которые ранее были взяты под контроль//Коллбеки не вызываетvoid unmount_all_devices();

Тут немного поясню. Функция mount_existing_devices необходима для того, чтобы класс смог взять под свой контроль все флешки, которые уже существовали в системе до его создания. Если этого не сделать, мы не сможем манипулировать ими и отслеживать их состояние, так как по умолчанию класс подхватывает только те устройства, которые были вставлены уже после его создания. С unmount_all_devices все ясно - эта функция просто освобождает все ранее взятые под контроль флешки, после чего класс более их не отслеживает и не занимает. Никакие коллбеки при этом не вызовутся.

//Безопасно извлечь какое-либо устройство//Коллбеков не вызываетvoid safe_eject(char letter);

Эта функция позволяет произвести принудительное безопасное извлечение устройства по букве диска, как будто пользователь кликнул "Безопасное извлечение устройства". Если флешка была подконтрольна классу, он перед отключением ее освободит. Никакие коллбеки при этом вызваны не будут.

//Установить опцию - отключать ли безопасно USB-устройство даже в том случае,//если после запроса на отключение от Windows прошел таймаут ожидания ответа//от приложения//По умолчанию включеноvoid set_safe_remove_on_timeout(boolremove);
 
    //Включена ли опция безопасного отключения после таймаута ожидания Windowsbool is_set_safe_remove_on_timeout_on()const;

Эти функции реализуют тот функционал, который я описал выше - принудительное безопасное извлечение после истечения таймаута ожидания ответа на ивент DBT_DEVICEQUERYREMOVE.

Наконец, парочка вспомогательных функций:

//Получить буквы всех USB flash-дисков, имеющихся в системе в данный момент времени//Если include_usb_hard_drives == true, то в список попадут буквы внешних жестких дисков,//в противном случае - только флешкиstatic std::set<wchar_t> get_flash_disks(bool include_usb_hard_drives);
 
    //Вспомогательная функция для консольных приложений//Написал ее чисто для теста, вы наверняка будете организовывать//цикл сообщений Windows как-то по-другомуstaticvoid message_pump();

Настройки отслеживания внешних жестких дисков:

//Включить опцию отслеживания внешних жестких дисков//По умолчанию включена опция отслеживания только USB-флешекvoid enable_usb_hard_drive_monitoring(bool enable);
 
    //Включена ли опция отслеживания внешних жестких дисковbool is_usb_hard_drive_monitoring_enabled()const;

Деструктор:

//Деструктор
    ~usb_monitor();

Теперь - немного о закрытых переменных и функциях класса:

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_monitorstatic 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;};
 
    //Эта функция возвращает информацию об устройстве по букве дискаstaticconst 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_;
 
    //Имя класса регистрируемого окна, об этом далееstaticconst std::wstring class_name;};

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

//Инклюды#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;returntrue;}else{returnfalse;}}
 
//Будет вызвано при ошибке безопасного извлечении какого-либо диска//(таймаут или запрет извлечения)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();
 
    return0;}

Данный пример будет мониторить все USB-диски и флешки в системе, позволит манипулировать безопасным извлечением. Работать будет на системах от Windows XP/2003 до Windows 7 (на восьмерке не проверял, но скорее всего тоже будет все нормально). Разумеется, класс usb_test нельзя будет использовать в службах Windows - потребуется небольшая переработка. Но в любых пользовательских приложениях - запросто.

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

Далее я начну более детальное описание кода для тех, кому интересно, как это все реализовано и работает. Незаинтересованных прошу сразу в конец статьи за файлами.

//Инклюды#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:

//Указатель, содержащий единственный инстанс класса//(напоминаю, что usb_monitor - синглтон)
std::auto_ptr<usb_monitor> usb_monitor::instanse_;//Имя регистрируемого класса окна для отслеживания USB-девайсовconst std::wstring usb_monitor::class_name(L"UsbMonWinClass");

Начнем разбор основного функционала с конструктора:

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));}

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

//Статическая процедура, в которую сыпятся все оконные сообщения
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)returnreinterpret_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_DEVICEQUERYREMOVEif(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, которая в коде выше используется дважды. Она определяет, какие с момента последнего ее вызова устройства были добавлены и удалены. Разберем ее код:

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:

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 все проще. Первый параметр - буква диска, а второй сообщает, следует ли вызывать коллбек пользователя, чтобы сообщить о том, что устройство извлечено небезопасно.

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;}}}

Теперь - еще две коротенькие вспомогательные функции:

//Освободить все взятые под контроль устройства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. Смысл ее понятен, осталось разобрать содержание.

//Параметр include_usb_hard_drives говорит о том, стоит ли включать в список
std::set<wchar_t> usb_monitor::get_flash_disks(bool include_usb_hard_drives){
    std::set<wchar_t> devices;
 
    //Получаем список логических разделовunsignedint 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);//то это флешка}//Если это какой-то жесткий диск, и мы их тоже мониторимelseif(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, и немного переписан.

//Структура 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-ROMbreak;
 
    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;}

Основная самая умная часть кода завершилась. Остались мелочи, некоторые из которых я тем не менее опишу. Для начала, функция безопасного извлечения устройства по букве его диска.

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:

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

Также рекомендую почитать

Игрища с USB в Windows (отслеживаем и контролируем) Обсудить на форуме



Оставьте ваш комментарий

 

 

Источник: http://feedproxy.google.com/~r/kaimi/dev/~3/6I85HnBygWc/

Читать комменты и комментировать

Добавить комментарий / отзыв



Защитный код
Обновить

Игрища с USB в Windows (отслеживаем и контролируем) | | 2012-07-19 23:29:00 | | Блоги и всяко-разно | | HTTP/1.1 200 OK Server: nginx Date: Sat, 08 Sep 2012 10:19:10 GMT Content-Type: text/html; charset=UTF-8 Transfer-Encoding: chunked Connection: keep-alive Vary: Accept-Encoding Vary: Cookie | РэдЛайн, создание сайта, заказать сайт, разработка сайтов, реклама в Интернете, продвижение, маркетинговые исследования, дизайн студия, веб дизайн, раскрутка сайта, создать сайт компании, сделать сайт, создание сайтов, изготовление сайта, обслуживание сайтов, изготовление сайтов, заказать интернет сайт, создать сайт, изготовить сайт, разработка сайта, web студия, создание веб сайта, поддержка сайта, сайт на заказ, сопровождение сайта, дизайн сайта, сайт под ключ, заказ сайта, реклама сайта, хостинг, регистрация доменов, хабаровск, краснодар, москва, комсомольск |
 
Поделиться с друзьями: