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

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

Не так давно появилась задача - каким-то образом отслеживать появление новых 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). Код для этого я не писал, но разобраться проблемы не будет, так как никаких отличий в начинке этого кода по сути нет.

📎📎📎📎📎📎📎📎📎📎