Thunderbird w zasobniku systemowym - The hard way!

tech • 1419 słów • 7 minut czytania

Początkowo planowałem zaprosić wszystkich na serię kilku odcinków o hakowaniu i programowaniu pod Windowsem. Miały to być zapiski z mojej próby rozszerzenia funkcjonalności programu, ale bez modyfikacji jego źródeł i binarek, tylko na “żywca” w czasie jego działania. Zapowiadał się ciekawy po-wakacyjny projekt, ale tym razem coś nie wyszło ;)

Minimize To Tray

Odkąd przesiadłem się na Mozillowego Thunderbirda brakowało mi w nim tego co bardzo ceniłem w poprzednio używanych klientach pocztowych. Czegoś tak prostego jak minimalizacja do SysTray-a. Niby są dostępne wtyczki i rozszerzenia, które przy próbie zamknięcia programu “wpakują” go do zasobnika systemowego, ale różnie bywa z ich działaniem1.

Od dawna nosiło mnie, aby “dorobić” sobie taką funkcjonalność. Program jest rozpowszechniany na wolnych licencjach, więc są dostępne kody źródłowe. Ale pójście tą droga byłoby nudne - bo co to za frajda pogrzebać w projekcie i dodać kilka nowych fragmentów kodu. A także problematyczne - bo później te moje kawałki kodu trzeba ogarniać przy każdym nowym wydaniu, a co gorsza samemu kompilować i budować cały pakiet. A komu by się chciało…

Można do tego podejść też od drugiej strony - hakować program w locie już w czasie jego działania, czyli modyfikować kod w pamięci, hookować funkcje, wstrzykiwać DLL-ki, subclassować okna i babrać się w gołym WinAPI i bebechach systemu. Nawet nie pamiętam kiedy ostatnio miałem z tym wszystkim styczność i jaką frajdę mi to sprawiało.

Plany były ambitne, ale w czasie pisania tego tekstu natrafiłem na informacje, że nowy Thunderbird w wersji 782 doczekał się już tej użytecznej opcji - “Minimize to Tray”. No i tym sposobem projekt umarł w przedbiegach. Długo się do niego zbierałem i może gdybym od razu ruszył z pracą, gdy pojawił się w mojej głowie pomysł, to teraz wyglądałoby to inaczej. A tak chyba nie warto się już z tym babrać i dublować natywnej funkcjonalności.

Powinienem zmienić tytuł z “hard way” na “easy way”, bo wystarczy zainstalować nową wersję programu i gotowe ;)

Jak to miało działać…

Może nie ma sensu się w to bawić, niemniej miałem już jakieś plany i wizję działania, więc warto chociaż po krótce przedstawić (do celów archiwalnych) jak to w moim zamierzeniu miało funkcjonować…

Całość miała składać się z dwóch elementów - loadera (TbSysTrayLoader) i DLL-ki z “ładunkiem” (TbSysTrayDLL). Głównym zadaniem loadera byłoby utworzenie procesu Thunderbirda i wstrzykniecie do niego biblioteki TbSysTrayDLL, która to wykonywałaby całą czarną robotę już bezpośrednio we wnętrzu działającego programu.

Do wstrzykiwania chciałem zastosować mechanizm APC, co przeprowadzone w odpowiedni momencie pozwoliłoby na wykonanie kodu przed wejściem do funkcji main programu. Wstrzykiwany payload byłby napisany ręcznie w asemblerze i przez nasm-a asemblowany do kodu maszynowego. Postać binarna zapewne wylądowałby w kodzie źródłowym loadera w postaci tablicy. Shellcode odpowiadałby za załadowanie modułu (LoadLibrary) i ewentualnie odpalenie dedykowanej funkcji inicjującej działanie (Run), jeśli miało to być coś innego poza DllMain.

Moduł biblioteki DLL działający w przestrzeni adresowej procesu Thunderbirda musiałby wyłapywać “momenty” tworzenia okien, aby “przechwycić” główne okno aplikacji3. Do tego celu mógłby zakładać hooka na używaną (xul.dll) funkcję CreateWindowExW. Zamiast babrać się niskopoziomowo w trampolinę, modyfikacje IAT/EAT, bardziej skłaniałbym się tutaj do użycia SetWindowsHookEx i założenia lokalnego hooka WH_CBT (HCBT_CREATEWND).

Z otrzymywanych “notyfikacji” o powstających oknach wyodrębnienie tego dotyczącego głównego okna programu powinno być łatwe. W Thunderbirdzie MainWindow ma ustawiony styl WS_OVERLAPPEDWINDOW (jak w każdej okienkowej aplikacji pod Windowsem) i tworzony jest z klasy okna MozillaWindowClass z pustą nazwą okna.

Mając już uchwyt HWND interesującego okna można przystąpić do zabawy, czyli subclassowania okna. Manewr ten dokonałbym jedną z dostępnych funkcji - starodawnym SetWindowLong podmieniając bezpośrednio procedurę obsługi okna (GWL_WNDPROC) lub nowocześniejszym SetWindowSubclass.

W typowej okienkowej aplikacji Win32 wystarczyłoby przechwycić komunikat WM_CLOSE i zablokować zamykanie programu, wykonując minimalizację. Niestety w aplikacjach XUL-owych, gdzie bogate API pozwala na wiele możliwości modyfikowania zachowania programu przez wtyczki lub inny kod, standardowy przepływ komunikatów może być zaburzony. I taki komunikat może w ogóle się nie pojawiać lub w czasie daleko już posuniętego zamykania programu.

Lepsze wydaje się reagowanie od razu na naciśnieci ikonki lub danej pozycji z menu systemowego, czyli “własna” obsługa WM_SYSCOMMAND przy żądaniu SC_CLOSE. To też, w pewnych okolicznościach, może z tych samych powodów nie zadziałać. Thunderbird sam maluje belkę z tytułem okna, bo umieszcza tam jakieś swoje kontrolki i treści. Wszystko zależy od tego, czy kod podażą za standardowymi zaleceniami i wysyła odpowiednie wiadomości do okien.

Gdy to również zawiedzie, można przejść do kolejnej możliwości, czyli łapanie zdarzeń myszki na obszarze nie-klienckim okna - WM_NCLBUTTONUP. Możliwe jest wtedy wykrycie, czy użytkownik kliknął w przycisk zamykający HTCLOSE, dzięki informacji zawartej w wParam, pochodzącej z wcześniejszego “wykonania” WM_NCHITTEST.

Jak już wspomniano, wszystko zależy od tego, czy przy ręcznym malowaniu program wysyła odpowiednie komunikaty. Robić tego nie musi, bo sam bierze na siebie odpowiedzialność obsługi poszczególnych akcji. Przy testach zauważyłem, że nawet ustawienia systemu mają na to jakiś wpływ. Przy włączonym Aero na 7-ce, cześć tych komunikatów leciała poprawnie, ale na testowej VM-ce z 7-ką bez Aero już niekoniecznie, a niektóre w ogóle się nie pokazywały.

To oczywiście też, mniej lub bardziej żmudnymi działaniami, dałoby się jakość obejść - na przykład samemu zidentyfikować pozycje przycisków, a te standardowo i tak malowane są zgodnie z systemowym stylem, i odpowiednio reagować na akcje użytkownika. Trochę to upierdliwe, ale czasem może nie być innej opcji…

W czasie budowaniu głównego okna, wstrzykiwany kod dodaje ikonkę do zasobnika systemowego. Wszelkie manipulacje ikonką w “obszarze powiadomień” dokonuje się za pomocą funkcji Shell_NotifyIcon. Jednocześnie kod resetuje w oknie głównym styl WS_EX_APPWINDOW, przez co okno nie pojawi się na pasku zadań.

Obiekt ikonki pobierany jest wprost z zasobów programu (LoadIcon + MAKEINTRESOURCE) lub z tworzonego okna (GetClassLong + GCL_HICON), przez to nie ma potrzeby jakiegokolwiek dołączania fizycznej ikonki do wstrzykiwanego kodu ani modułu. Na zdarzenie WM_LBUTTONDBLCLK wysłane z systray-owej ikonki program reaguje pokazaniem okna na ekranie za pomocą funkcji ShowWindow lub pochodnej.

Dodatkowo do ikonki można byłoby podłączyć jakieś główne menu programu (na przykład te spod pozycji File). Dobrać się do menu można “przechwytując” moment jego tworzenia, korzystając z funkcji GetMenu lub po prostu skopiować obiekt oryginalnego menu programu. Szczegóły ściśle zależą od sposobu implementacji tych elementów w XUL.

Innym “bonusem” mogłaby być animacja ikonki w SysTray-u jako sygnalizacja otrzymania nowej poczty…

Idea permanentnego “wywalenia” okna z paska zadań i rezydowania aplikacji tylko w obszarze powiadomień jest moim zdaniem jedynym słusznym sposobem implementacji mechanizmu minimalizacji programu do SysTray-a. Program nie jest w ogóle widoczny na pasku zadań, jego okno pojawia się na ekranie tylko po kliknięciu ikonki, a chowa się przy próbie jego zamknięcia lub minimalizacji okna. Odzwierciedla to działanie innych elementów i aplikacji systemowych rezydujących tylko w SysTray-u.

Niektóre aplikacje implementują w ten sposób minimalizację, ale większość robi to połowicznie - nie pokazuje okna na pasku zadań, ale tylko w czasie stanu zminimalizowanego/ukrytego okna, a minimalizacja wyzwalana jest tylko przy kliknięciu przycisku Minimize. Takie działanie i zachowanie mi nieszczególnie się podoba.

I to całość skróconego opisu działania jakie chciałem zaimplementować :)

I co dalej?

Projekt miał przede wszystkim zaspokoić moje potrzeby wynikłe z braku pożądanej funkcji w programie. Oprócz użytecznego aspektu, miał też być dobrą zabawą i fajnym, edukacyjnym przykładem praktycznej modyfikacji zachowania programu w run-time. Nie chciałbym zmarnować potencjału jaki we mnie drzemał i przeprowadzonych już prób i testów. Dlatego zdecydowałem się na napisanie kilku luźnych i niepowiązanych notek poruszających wybrane zagadnienia techniczne i rozwiązania jakie chciałem wykorzystać w tym projekcie.

Myślę, że na pewno pojawią się wpisy o wstrzykiwaniu kodu przez APC i subclassowaniu okien, bo to w końcu miały być jakby fundamenty mojego rozwiązania. Może też napiszę coś o samym hookowaniu okien i funkcji w Windowsie, czy innych tego typu bzdetach… Może nie jest to jakaś tajemna wiedza, ale rzadko się o tym pisze…

[dodano 2020-09-01 22:00]

Zainstalowałem nowego klienta i rzeczywiście wśród wielu zmian i “ulepszeń” w opcjach można znaleźć checkboxa “Minimalizuj program Thunderbird do ikony w obszarze powiadomień”. Niestety nie do końca spełnia to moje oczekiwania. Reaguje tylko na minimalizację, czy to za pomocą przycisku i menu systemowego okna, czy po kliknięciu na pasku zadań. Brakuje mi tutaj dodatkowej opcji w stylu “Minimalizuj przy próbie zamknięcia programu”, aby kliknięcie ikony zamykającej okno także zminimalizowało do tray-a. Nie zmienia to jednak mojego zdania co do tego projektu ;)


Przypisy

  1. Sam kilka takich dodatków używałem, ale ostatnio najlepiej spisywał się “MinimizeToTray Reanimated”. Niestety po pierwszej aktualizacji Thunderbirda w tym roku nagle przestał działać z powodu “kompatybilności”. Zapewne związane jest to z odchodzeniem Mozilli od XUL/XPCOM-owych wtyczek na rzecz “webowych” rozszerzeń. ↩︎

  2. Dopiero teraz natrafiłem na info o wersji 78, bo wciąż nie ma dostępnej automatycznej aktualizacji z wersji 68 i wcześniejszych… ↩︎

  3. Potrzebny jest moment tworzenia głównego okna, aby można było zaimplementować opcję “minimalizuj przy stracie programu”. ↩︎

Komentarze (0)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/