OpenCppCoverage in Docker
« | 20 Nov 2021 | »Wieso (zur Hölle) funktioniert OpenCppCoverage eigentlich nicht in Windows Docker Containern?
Denn während man auf normalen Desktop Rechnern und Servern mit OpenCppCoverage schöne Statistiken erhält, welche Codezeilen durchgelaufen sind (und welche nicht berührt wurden), erhalten wir mit dem selben Tool im Container die Meldung:
cannot find the name of the module
Spoiler: Docker wird von OpenCppCoverage aktuell leider nicht unterstützt
und es gibt aktuell keinen Workaround für das Problem.
Aber: Vielleicht können wir dem Thema demnächst mal nachgehen und eine
Teillösung ausprobieren.
Vorgeschichte - Neulich in der Firma …
Der Chef will bunte Tortendiagramme, ob und wie viel unsere Unit-Tests den Code
wirklich abprüfen.
Manuell konnte man dies immer schon am Desktop-Rechner mit OpenCppCoverage mit
einem Plugin in Visual Studio
tun, doch eigentlich ist es ja viel einfacher, wenn am Build-Server beim
Ausführen aller Tests auch ein Coverage-Report generiert wird.
… tja, und der Build-Server baut eben auf Docker
auf und dort wird jede Coverage mit dem Fehler cannot find the name of the module
abgewürgt.
Analyse
Der bzw. die OpenCppCoverage Entwickler haben ein kleines Test-Tool erstellt, welches das Problem aufzeigt:
Benutzt wird die Windows Debug API
und diese liefert normalerweise bei jedem Ladevorgang einer
DLL eine File-Handle
(hfile
) zum geöffneten Modul.
So kann man dann Debug-Symbole aus dem Modul laden und die richtigen
Quellcode-Zeilen zum ausgeführten Binärcode ausfindig machen.
Unter Docker liefert die Debug-API (WaitForDebugEvent()
) aber offenbar
nur NULL
für hfile
, was die weitere Code-Coverage verhindert.
Ich habe daher mal ebenfalls ein Test-Projekt mit Debugging-API-Code zusammengebastelt um das bei mir besser nachvollziehen zu können. Die Quellcodes hierfür sind im CLASSROOM veröffentlicht.
Und ja, hfile
ist tatsächlich NULL
, wenn man z.B. cmd.exe
damit
debuggen möchte um seine Module im LOAD_DLL_DEBUG_EVENT
zu erkennen.
Danach habe ich einen Ordner meines Hosts in den Container gemountet um andere
Tools zu testen und wieder war der hfile
Member auf NULL
gesetzt.
Danach machte ich aber etwas ganz Verrücktes:
Ich erzeugte im Docker-Container einen Test Ordner auf Laufwerk C:\test
und kopierte meine Test-Programme vom gemounteten Host-Ordner dort hin.
Kaum startete ich den Debugging-Test, kamen auch schon die ersten korrekten
hfile
Werte, die mit GetFinalPathNameByHandleW()
in einen Dateipfad umgewandelt werden konnten.
Die Pfad-Angaben sind im NT-Format, z.B.: \\\\?\\c:\\path\\to\\module.dll
.
Teil-Lösung 1: Man muss Dateien in einen Container-interne Ordner kopieren, damit die Debug-APIs gültige Werte liefern. Denn mit Host-Volumes funktioniert es nicht.
Das Verhalten (wenngleich sehr verstörend) ist sogar nachvollziehbar, schließlich handelt es sich um “virtuelle Pfade”, die ins Dateisystem von Docker “hineingehackt” werden. Dass die von manchen APIs anders behandelt werden, ist zwar lästig, vielleicht aber auch sicherheitstechnisch notwendig.
Nachteil: Dieser Workaround läuft letztendlich nicht mit allen Modulen.
ntdll.dll
ist da trotzdem ausgenommen, und Tools wie cmd.exe
und andere
System-DLLs liegen grundsätzlich an einem Ort, der aus anderen Docker
Layern zusammengeklebt ist. Die kann man da nicht weg kopieren und somit
fehlen ihre Daten.
… aber für die Code-Coverage wäre das egal, da interessieren wir uns ja
für “unseren” Code und nicht für fremde System-DLLs.
ImagePath im Remote-Prozess
Wenn es uns weiters um den Image-Pfad eines geladenen Moduls geht, gibt es noch eine weiter Möglichkeit:
LOAD_DLL_DEBUG_INFO
führt nämlich noch die Member lpImageName
und fUnicode
mit sich.
lpImageName
hält die Speicheradresse zu einem Pointer zu einem String mit
dem Pfad des Moduls. Dieser Parameter ist zwar nur optional befüllt, doch in
meinen Tests war er auch im Docker Container stets ausgefüllt.
Das Problem ist nur, der Pointer verweist auf Speicher im Remote-Prozess. Wir müssen daher mit der ReadProcessMemory Zeichen für Zeichen herauspicken.
Mein Demo-Code hierfür sieht etwa so aus:
1static int print_remote_image_path(HANDLE hProcess, void* lpImageName, int unicode) 2{ 3 int chars_printed = 0; 4 char* remote_address = NULL; 5 SIZE_T returned = 0; 6 SIZE_T char_len = unicode ? sizeof(wchar_t) : sizeof(char); 7 wchar_t buffer[4] = { 0 }; 8 char* ptr_char = (char*)&buffer[0]; 9 10 if(ReadProcessMemory(hProcess, lpImageName, &remote_address, sizeof(remote_address), &returned)) 11 { 12 if(remote_address != NULL) 13 { 14 while(ReadProcessMemory(hProcess, remote_address, ptr_char, char_len, &returned)) 15 { 16 if(unicode) 17 { 18 if(buffer[0] == 0) break; 19 wprintf(L"%c", buffer[0]); 20 } 21 else 22 { 23 if(*ptr_char == 0) break; 24 printf("%c", *ptr_char); 25 } 26 remote_address += char_len; 27 ++chars_printed; 28 } 29 } 30 } 31 return chars_printed; 32}
Man lädt also zuerst den Pointer zum String über die Adresse in lpImageName
,
und je nachdem, ob es ein Unicode oder ANSI String ist, wird Byte für Byte
aus der Zieladresse gezogen, bis wir einen \0
Terminator finden,
welcher das Ende des String andeutet.
Teil-Lösung 2: Wir extrahieren den Pfad des geladenen Moduls aus dem Zielprozess und öffnen das Modul manuell.
Nachteil: Der Parameter ist als “optional” markiert und ich weiß auch noch nicht, ob dieser Pfad und die manuelle Öffnung dann für eine Coverage benutzt werden kann.
Fazit
Die schnellere Lösung war es natürlich, mit dem GCC
und anderen Tools eine vergleichbare Code-Coverage unter Linux
laufen zu lassen.
Es ist generell ärgerlich, dass unter Windows viele OpenSource-Tools bis
heute Kinderkrankheiten haben und nur sündteure “Enterprise-Produkte”
Abhilfe versprechen.
Wie auch immer … es wäre mal ein netter Sidequest, den OpenCppCoverage Code zu studieren und vielleicht lassen sich die beiden beschriebenen Teillösungen dazu nutzen, auch mit diesem eigentlich großartigen Tool neue Coverage-Reports unter Windows Docker Containern erstellen zu lassen.
… mal sehen, was die Zukunft bringt.