Как оказалось, библиотека для LastFM из статьи от 31.08.11 оказалась не особо юзабельной из-за нескольких досадных недоработок:
1. Клавиши управления не работали, если окно свернуто (связано с тем, что управление производилось с помощью SendMessage, но в свернутом состоянии окно не обрабатывает оконные сообщения, поэтому они переставали реагировать). (Спасибо Artik'у за наводку)
2. При сворачивании окна в трей и последующем восстановлении пропадали элементы управления, так как окно "уничтожалось".
Чтобы исправить эти моменты, я решил переписать библиотеку. В этой статье я опишу изменившиеся методы (к слову, CreateThumbnailToolbar и EnumWindowsProc остались без изменений).
Итак, начнем с глобальных переменных, которые пришлось добавить:
1 2 3 4 5 6 7 8 9 10 |
//Идентификатор таймера UINT_PTR timer = NULL; //"Переключатель" для отслеживания необходимости повторного добавления //элементов управления окну BOOL re_enable = TRUE; //Содержит адрес this для вызова некоторых методов напрямую из клиента LastFM DWORD ESI_ = NULL; //Адрес функции Qt, которая позволяет проверить видимость главного окна клиента DWORD QtIsVisibleWidget = NULL; |
Теперь несколько новых функций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
//Вызов функции по её абсолютному адресу DWORD CallFuncByAddr(DWORD addr) { __asm { mov eax, addr call eax } } //Функция для установки своеобразного "хука" на процедуру клиента, //в которой идет обработка событий void HookUnhook(BOOL do_hook) { //0x00440160 - Адрес функции обработчика событий, //о получении адреса написано ниже DWORD pr, addr = 0x00440160; VirtualProtect((LPVOID)addr, 1, PAGE_EXECUTE_READWRITE, &pr); CopyMemory((void *)addr, do_hook ? "\xCC" : "\x55", 1); VirtualProtect((LPVOID)addr, 1, pr, &pr); } //Обработчик векторных исключений, он снимает "хук" и сохраняет //содержимое регистра ESI (this) для дальнейшего использования LONG CALLBACK VEH(PEXCEPTION_POINTERS ExceptionInfo) { HookUnhook(FALSE); //В ESI всегда лежит указатель на текущий объект, //который нам пригодится в дальнейшем ESI_ = ExceptionInfo->ContextRecord->Esi; return EXCEPTION_CONTINUE_EXECUTION; } |
Теперь о том, как я нашел процедуру обработки и определил, какие методы из неё меня интересуют. Нажимаем кнопку Play в свежеустановленном LastFM, получаем сообщение о невозможности продолжить проигрывание, открываем клиент LastFM в отладчике (я пользовался OllyDbg), ищем все строки, упомянутые в секции кода текущего модуля (Search for -> All referenced text strings), находим текст сообщения.
Переходим в функцию, которая ссылается на эту строку, ищем, откуда эта функция вызывается, и находим процедуру, в которой обрабатываются все интересующие нас события.
Остается "потыкать" на интересующие нас кнопочки и определить, какие функции вызываются при их нажатии. Адреса получились следующие:
1 2 3 4 |
Play - 0х0048E000 Stop - 0х0048C4E0 Skip - 0х0048DC50 Like - 0х0048DEF0 |
Теперь перепишем процедуру WndProc, чтобы избавиться от проблемы №1.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { if(msg == WM_COMMAND && ESI_ != NULL) { switch(LOWORD(wParam)) { //BUTTON 1,2,3 - идентификаторы кнопок управления, //описанные в предыдущей статье case BUTTON1: //Т.к. одна клавиша выполняет сразу две функции (Play и Stop), //то необходимо определять, играется ли в данный момент композиция или нет. //Проще всего это определить по длине заголовка окна if(GetWindowTextLength(ghWnd) == 7) { //Play CallFuncByAddr(0x0048E000); } else { //Stop CallFuncByAddr(0x0048C4E0); } break; case BUTTON2: //В ECX помещается this, т.к. вызывается метод объекта //mov ecx, esi - это фрагмент, стоящий перед вызовом функции, //который скопирован из клиента LastFM _asm mov ecx, ESI_ //Skip CallFuncByAddr(0x0048DC50); break; case BUTTON3: //Фрагмент скопирован из клиента LastFM _asm mov edi, ESI_ //Like CallFuncByAddr(0x0048DEF0); break; } } return CallWindowProc(old_proc, hWnd, msg, wParam, lParam); } |
Почему я использую статичные смещения для вызова функций? Во-первых, у клиента LastFM отсутствует секция релокаций, а следовательно, он будет грузиться по смещению, указанному в ImageBase.
Во-вторых, клиент довольно редко обновляется, что продлевает актуальность этой надстройки.
Наконец, остается решить проблему №2 - сделать, чтобы кнопки управления восстанавливались после закрытия и повторного открытия окна. Для этого проще всего создать таймер, который будет проверять видимость главного окна и восстанавливать кнопки управления.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) { if(QtIsVisibleWidget && ESI_) { _asm mov ecx, ESI_ //Вызываем внутреннюю функцию Qt, чтобы определить наличие основного окна if(CallFuncByAddr(QtIsVisibleWidget)) { EnumWindows(&EnumWindowsProc, GetCurrentProcessId()); if(re_enable) { re_enable = FALSE; CreateThumbnailToolbar(ghWnd); if(!old_proc) old_proc = (WNDPROC)GetWindowLong(ghWnd, GWL_WNDPROC); SetWindowLong(ghWnd, GWL_WNDPROC, (LONG)WndProc); } } //Если окно было уничтожено, устанавливаем флаг, чтобы при следующем //появлении окна в него были добавлены элементы управления else { re_enable = TRUE; } } } |
И последняя измененная функция - DllMain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID reserved) { if(dwReason == DLL_PROCESS_ATTACH) { //Определяем адрес, по которому загрузилась библиотека QtGui4 HMODULE qtgui = GetModuleHandle(L"QtGui4.dll"); //Добавляем к адресу модуля RVA интересующей нас функции QtIsVisibleWidget = (DWORD)qtgui + 0x0000EF50; //Добавляем обработчик векторных исключений AddVectoredExceptionHandler(1, VEH); //Устанавлиаем "хук" HookUnhook(TRUE); CoInitialize(NULL); ghInstance = hInstance; //Запускам таймер, который будет срабатывать каждые 3 сек timer = SetTimer(NULL, 0, 3000, TimerProc); } else if(dwReason == DLL_PROCESS_DETACH) { CoUninitialize(); //Отключаем таймер KillTimer(NULL, timer); } return TRUE; } |
Исходный код и скомпилированная DLL: скачать
Спасибо)
Кстати, радио можно слушать бесплатно и в других странах, достаточно скачать старый клиент скробблера.
Например отсюда: http://rutracker.org/forum/viewtopic.php?t=3737124
Надо будет посмотреть в чем разница старого и нового клиента, возможно удастся для нового клиента сделать патч или DLL для бесплатного прослушивания в нём.
Было бы неплохо, хотя в принципе различий между новым и старым клиентом почти никаких.
http://habrahabr.ru/post/145318/