From 9370bff31954372b6f1f63e21339f1b067547fa2 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 3 Sep 2026 03:19:23 -0400 Subject: [PATCH] =?UTF-8?q?[ADD]=20qemu=20diagnostic=20:=20le=20device=20v?= =?UTF-8?q?id=C3=A9o=20re=C3=A7u=20par=20le=20QEMU=20en=20cours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le relevé disait ce que la définition DEMANDE, jamais ce que la VM a REÇU ; libvirt peut retirer l'accélération entre les deux sans le signaler. Or « egl-headless » paraît sur la ligne de commande que la 3D soit active ou non : le seul témoin est le suffixe « -gl » du device. Chercher « virgl » n'aide pas — cette forme ne s'écrit plus sur les QEMU récents, et son absence sur une VM accélérée fait conclure à un réglage manquant qui n'existe pas. La sonde lit /proc//cmdline, sans privilège, et trie sur argv[0] : sur la ligne entière elle se compterait elle-même. --- EN --- The report told what the definition ASKS for, never what the VM GOT; libvirt can drop acceleration in between without saying so. And « egl-headless » shows on the command line whether 3D is live or not: the only witness is the device's « -gl » suffix. Grepping for « virgl » does not help — that spelling is gone from recent QEMU, and its absence on an accelerated VM suggests a missing setting that does not exist. The probe reads /proc//cmdline, unprivileged, and sorts on argv[0]: on the whole line it would count itself. Assisted-by: Claude Opus 5 --- script/todo/qemu_manage.py | 40 +++++++++++++++++ test/test_qemu_start_egl.py | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/script/todo/qemu_manage.py b/script/todo/qemu_manage.py index f833cc3..5f4788d 100644 --- a/script/todo/qemu_manage.py +++ b/script/todo/qemu_manage.py @@ -109,6 +109,33 @@ def ssh_orphans(blocs, juge, prefixe="erplibre-"): return gardes, orphelines +# Programme de la sonde « video du qemu en cours » du diagnostic. Il vit +# ici, et non dans un littéral au milieu de la table des sondes : un +# programme Python cité dans une commande shell citée dans une chaîne +# Python porte trois niveaux d'échappement, et le premier guillemet le +# casse. `shlex.quote` s'occupe du seul niveau qui reste. +_DIAG_VIDEO_PY = """ +import glob + +MOTS = ("vga", "virtio-gpu", "egl", "virgl", "rendernode") +for chemin in sorted(glob.glob("/proc/[0-9]*/cmdline")): + try: + with open(chemin, "rb") as fh: + argv = fh.read().decode("utf-8", "replace").split(chr(0)) + except OSError: + continue + # Le tri se fait sur argv[0] : sur la ligne entière, la sonde se + # verrait elle-même, son propre motif contenant « qemu-system ». + if not argv or "qemu-system" not in argv[0]: + continue + noms = [a[6:].split(",")[0] for a in argv if a.startswith("guest=")] + print(noms[0] if noms else chemin.split("/")[2]) + for arg in argv: + if any(mot in arg.lower() for mot in MOTS): + print(" " + arg) +""" + + class QemuManageMixin: """Menu QEMU/KVM : g\u00e9rer les VM existantes.\n\nLe cycle de vie apr\u00e8s la cr\u00e9ation : lister, allumer et \u00e9teindre, r\u00e9gler le\nmat\u00e9riel, redimensionner (et r\u00e9tr\u00e9cir, ce qui demande de traverser le syst\u00e8me\nde fichiers invit\u00e9 par nbd), effacer, nettoyer les restes, retrouver une\nadresse IP, rouvrir le suivi d'une installation.\n\nC'est le fichier qui appelle \u00ab virsh \u00bb le plus souvent : les helpers qui le\nfont (domstate, dumpxml, c_env) vivent donc ici.""" @@ -462,6 +489,19 @@ class QemuManageMixin: "utilisateur des VM", "ps -o user=,comm= -C qemu-system-x86_64 2>&1 | sort -u", ), + # Ce que la VM a REÇU, et non ce que sa définition demande : entre + # les deux, libvirt peut avoir retiré l'accélération sans le dire. + # « virtio-vga-gl » est le device 3D des QEMU récents ; l'ancienne + # forme « virtio-vga,virgl=on » ne s'écrit plus, si bien qu'un grep + # sur « virgl » ne rend rien sur un QEMU pourtant accéléré et fait + # conclure à tort qu'il manque un réglage. Le suffixe « -gl » est ce + # qui distingue le device 3D de celui du rendu logiciel, et c'est la + # seule pièce que la ligne de commande dise sans ambiguïté : + # « egl-headless » s'y trouve dans les deux cas. + ( + "video du qemu en cours", + "python3 -c " + shlex.quote(_DIAG_VIDEO_PY) + " 2>&1", + ), ("stockage", "df -h /var/lib/libvirt/images 2>&1"), ("groupes", "id"), ) diff --git a/test/test_qemu_start_egl.py b/test/test_qemu_start_egl.py index 2f7cc40..948ff79 100644 --- a/test/test_qemu_start_egl.py +++ b/test/test_qemu_start_egl.py @@ -509,3 +509,91 @@ class _FauxModule: if __name__ == "__main__": unittest.main() + + +class LaSondeVideo(unittest.TestCase): + """La 3D d'une VM se lit sur la ligne de commande de son QEMU. + + La définition dit ce qui est DEMANDÉ ; la ligne de commande dit ce qui + a été REÇU. Entre les deux, libvirt peut retirer l'accélération sans le + signaler, et « egl-headless » s'affiche dans les deux cas — chercher ce + seul mot fait conclure à tort que la 3D est en place. Le suffixe + « -gl » du device est la pièce qui tranche. + """ + + ARGV = ( + "/usr/bin/qemu-system-x86_64", + "-name", + "guest=une-vm,debug-threads=on", + "-device", + "%s,id=video0,max_outputs=1,bus=pcie.0", + "-display", + "egl-headless,rendernode=/dev/dri/renderD128", + "-audiodev", + '{"id":"audio1","driver":"none"}', + ) + + def _sonder(self, device, argv0=None, extra=()): + """Sortie de la sonde devant un /proc bâti pour l'occasion.""" + from script.todo.qemu_manage import _DIAG_VIDEO_PY + + import subprocess + + with tempfile.TemporaryDirectory() as tmp: + proc = Path(tmp) / "proc" / "4242" + proc.mkdir(parents=True) + argv = list(self.ARGV) + argv[0] = argv0 or argv[0] + argv[4] = argv[4] % device + argv.extend(extra) + (proc / "cmdline").write_bytes("\0".join(argv).encode() + b"\0") + programme = _DIAG_VIDEO_PY.replace( + "/proc/", str(Path(tmp) / "proc") + "/" + ).replace('chemin.split("/")[2]', "chemin") + fini = subprocess.run( + [sys.executable, "-c", programme], + capture_output=True, + text=True, + ) + self.assertEqual(fini.returncode, 0, fini.stderr) + return fini.stdout + + def test_the_accelerated_device_is_reported(self): + sortie = self._sonder("virtio-vga-gl") + self.assertIn("une-vm", sortie) + self.assertIn("virtio-vga-gl", sortie) + + def test_software_rendering_is_told_apart(self): + """L'épreuve du rapport : les deux cas ne doivent PAS se lire pareil. + + « egl-headless » est présent des deux côtés ; un rapport qui ne + montrerait que lui laisserait croire la 3D acquise. + """ + accelere = self._sonder("virtio-vga-gl") + logiciel = self._sonder("virtio-vga") + self.assertIn("egl-headless", accelere) + self.assertIn("egl-headless", logiciel) + self.assertNotEqual(accelere, logiciel) + self.assertNotIn("virtio-vga-gl", logiciel) + + def test_a_process_merely_naming_qemu_is_ignored(self): + """Le tri se fait sur argv[0], et non sur la ligne entière. + + Un processus quelconque peut porter « qemu-system » dans ses + arguments — un pager ouvert sur un journal, et la sonde elle-même, + dont le programme contient le mot. Les compter ferait naître des + VM qui n'existent pas. Le contre-exemple porte donc le motif + AILLEURS qu'en tête, sans quoi il ne départage rien. + """ + argv = ["/var/log/qemu-system-x86_64.log"] + self.assertTrue(any("qemu-system" in a for a in argv)) + self.assertEqual( + self._sonder("virtio-vga-gl", argv0="/usr/bin/less", extra=argv), + "", + ) + + def test_the_diagnostic_carries_the_probe(self): + from script.todo.qemu_manage import QemuManageMixin + + sondes = dict(QemuManageMixin._DIAG_PROBES) + self.assertIn("video du qemu en cours", sondes)