Consola NextUp ERP Server are o zona pentru afișarea jurnalului cererilor venite de la diverșii consumatori conectați; acea zonă este un fel de casetă text ce nu poate fi modificată, în care se tot adaugă, fără a fi niciodată ajustat sau complet golit conținutul său, afară de situația opririi și repornirii manuale a server-ului.

Atmosferă de lucru
Din cauza construcției interne a componentei (DevExpress.XtraEditors.TextBoxMaskBox, am și găsit comportamentul documentat la un moment dat în zona de suport a DevExpress, reclamat de un alt utilizator) tot procesul descris determină creșterea non-liniară a memoriei ocupate de-ntreg programul, sistemul devenind instabil pentru o sesiune de lucru de aproximativ 25000 de cereri (cu sesiuni depășind lejer 100 000) executate-n secvență rapidă (genul programului: aplicație de transfer).
În mod normal nu ar fi trebuit să fie problema mea și m-aș fi dispensat bucuros de dânsa, și nu oricum, ci de la mare înălțime deasupra unui corp întins de apă adâncă. Pe de altă parte, era necesar ca proiectul să obțină semnătura pe procesul-verbal și a trebuit să mă-nvârt iute, rapid și fulgerător de-o sârmă fermecată ce-ar fi ținut loc de șurub, estimând ca pentru mine că o soluție complet și corect definită avea sa vină, și-am avut dreptate, mult mai târziu, aproape un an mai târziu.

Zona ce crea probleme
Cum de-am ajuns la concluzia că din cauza acelei componente de prezentare a jurnalului creștea memoria consumată, nu știu. Doar am amintit, creșterea nu era liniară: dacă luai dimensiunea în octeți versus cum creștea consumul, nu aveau nimic în comun. Ce-i drept, părea punctul logic de plecare, întrucât era singurul element la vedere ce stoca orice fel de date-n mod neconstrâns (însăși definiția expresiei a wild stab in the dark), iar prima soluție de-ncercat ar fi fost, în consecință, o modalitate automatizată de golire.
Aveam deja o oarecare experiență cu unelte de testare automată precum Selenium Web Driver, spre exemplu, și-am căutat ceva similar pentru aplicații WinForms scrise-n C#/.NET, cu intenția de-a manipula continutul jurnalului astfel încât:
– să-l golesc periodic, la un interval configurabil și
– dacă tot am ajuns aici, să-l salvez într-un fișier implementând și-o minimă funcție de rotatie (adică atunci când ajunge în preajma unei dimensiuni oarecare să-nceapă a scrie într-un nou fișier).

wfSpy
Am găsit – nu la Penețeu – pywinauto (documentație aici), autoportretizat succint drept Windows GUI Automation with Python, scopul de căpătâi fiind probabil automatizarea testării funcționale. Chiar dacă python nu e un limbaj în care scriu frecvent (nici măcar curent), chiar dacă mi s-a părut un pic bizar când am pus prima oară mâna pe el, în timp a ajuns să-mi facă mare placere să-l folosesc, mai cu seamă că-i tare spornic (ceea ce s-a reconfirmat și-n cazul de față).
Desigur, nu-mi permiteam luxul unor considerații filozofice sau de natură estetică, însă faptul că-ntr-o singură seară, cu un pahar de coniac alăturea, am avut un prototip perfect funcțional cântărește-n cele două balanțe – sufletească și pecuniară: m-a ajutat să pun o solutie pe masă, mi-a eliberat fondurile pe drept cuvenite și mi-a cumpărat suficient timp pentru a rafina soluția.

wfSpy – proprietăți componentă
În prezent este trecut pe linie moartă pentru că, din fericire, consola NextUp ERP Server are mai nou o identică funcție proprie: își golește singur fereastra și trimite conținutul vechi în jurnale pe disc. Asta, evident, nu-nseamnă că nu ramane o poveste interesantă, mai cu seamă că mi-am câștigat măcar bragging rights. Așadar, să v-arăt python-ul meu.
Astfel stabilite coordonatele tehnico-principialo-tehnologice, a fost nevoie de-o serie de informații privind ierarhia componentelor, astfel încât să fie rezonabil de precis identificate. Ce baftă pe mine, din nou, cu ajutorul dat activităților investigativ-operative de către wfSpy – The Windows Forms Spy Utility.
Odată lansat, detectează aplicațiile construite cu Windows Forms curent deschise-n sistem mai rapid decât serviciile de informații și, spre deosebire de dânșii, prezintă informații utile angajate într-un raport nescris de ChatGPT, cu tot cu ierarhie, deloc disimilar cu structura DOM oferită, bunăoară, de orice navigator Web ca parte a suitei de unelte întru depanare.
Există și posibilitatea de-a identifica direct, vizual, în stil color picker, o componentă anume dorită, deschizând apoi automat o fereastră cu proprietățile aferente. Frumos, elegant, cât de cât, rezultând următoarea ierarhie de interes:

Schița ierarhiei de componente
Se observă că unele au o valoare anume pentru proprietatea Name (tbServer pentru tab-ul unde-i plasată componenta de interes pentru noi), altele nu (câmpul propriu-zis ce m-a interesat pe mine, acolo fiind menținut textul, DevExpress.XtraEditors.TextBoxMaskBox). Un alt aspect important este că acel câmp de fapt nu-i utilizat direct, ci e folosit de o altă componentă, txtServerMessages, de tip DevExpress.XtraEditors.MemoEdit.
Cu informațiile de mai sus la purtător, se poate încropi rapid un program cu unic scop inițial de-a stabili cum e „văzută” (ce componente sunt recunoscute, care-s elementele de identificare, cum corespund celor găsite etc.) consola server NextUp ERP de către pywinauto (auto_id = automation id, valoarea fiind strâns legată de backend-ul folosit, pentru cel win32 returnându-se proprietatea Name, ipoteză confirmată lesnicios în practică și-n documentație):
from pywinauto import Application, WindowSpecification
#backend - what "system" API pywinauto should use
app:Application = Application(backend='win32')
app.connect(title_re="(.*)Administrare Server(.*)", visible_only=False)
#grab top window element
top_wnd: WindowSpecification = app.top_window()
test_el:WindowSpecification = top_wnd.child_window(auto_id="tbServer",
enabled_only=False,
visible_only=False)
test_el.print_control_identifiers()
Produce următorul rezultat:
Deci extrem de similar cu wfSpy. M-a preocupat în mod deosebit aflarea căii către inima acelui DevExpress.XtraEditors.TextBoxMaskBox, adnotat cu Edit. Cum nu e musai să ai un identificator explicit pentru a identifica un element, să-ncercăm totuși folosind direct Edit:
#grab top window element top_wnd: WindowSpecification = app.top_window() txt_cnt: WindowSpecification = top_wnd.child_window( auto_id="txtServerMessages", enabled_only=False, visible_only=False) #just following documentation test_el: EditWrapper = txt_cnt.Edit print("Control ID = " + str(test_el.control_id())) print("Automation ID = " + str(test_el.automation_id())) print("Class name = " + test_el.class_name()) print("Friendly class name = " + test_el.friendly_class_name())
Secvență ce produce (suficient de interesant, acel friendly class name reprezintă o variantă mai civilizată a conceptului de window classes):
Control ID = 1510588 Automation ID = Class name = WindowsForms10.EDIT.app.0.3ed7652_r6_ad1 Friendly class name = Edit
Odată analizată structura și modalitatea de adresare, am putut scrie un mic program ce-a rulat o buclă cu cinci iterații, dormind o secundă per iterație și golind de fiecare dată caseta respectivă cu ajutorul metodei EditWrapper::set_edit_text(). Deloc surprinzător, a funcționat.
Dar dacă, m-am întrebat, fereastra este minimizată? Aici s-a gripat un pic, întrucât set_edit_text() validează aume dacă este actionable, adică activă și vizibilă. Nu înțeleg măsura, nici n-am transpirat mult timp pe tema asta, mai ales că există o soluție – EditWrapper.send_message(), complet nerestricționată:
import ctypes import locale import six from pywinauto import win32defines from pywinauto.controls.win32_controls import EditWrapper def set_edit_text_direct(target_element: EditWrapper, text: str) -> None: if six.PY3: send_text = text else: send_text = text.encode(locale.getpreferredencoding()) send_buffer = ctypes.create_unicode_buffer(send_text, size=len(send_text) + 1) target_element.send_message(win32defines.WM_SETTEXT, 0, ctypes.byref(send_buffer))
Înglobând și ultimul aspect, am putut livra o improvizație funcțională ce-a permis rularea sistemului de transfer ce-a fost de fapt scopul proiectului contractat. În ape liniștite, apoi, de curiozitate și ca exercițiu exploratoriu, l-am adus în forma de pe github.

Rezultatul final