From 087ce3effb22b679857b175663d62de0efdade0e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 19 Aug 2026 07:26:54 -0400 Subject: [PATCH] [FIX] script todo: viser l'IDE par son nom, pas par la ligne de commande MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le filet de fermeture de PyCharm cherchait « /opt/pycharm » dans les lignes de commande. Or le script d'installation est passé en argument à ssh, et il contient ce chemin : le pkill a tué la session ssh qui portait une installation en cours. Elle est morte sans marqueur de sortie, et le tableau de bord a montré un sablier pendant 54 minutes. Exécuter la suite de tests suffisait à déclencher le défaut, puisqu'un test rejoue l'étape. Le filet vise désormais les NOMS de processus, bornés au compte courant. Mesuré dans la VM : par nom, 3 processus réels et aucun faux ; par ligne de commande, 4 — le ssh compris. Un test plante un témoin nommé « sleep » dont la ligne contient le chemin de l'IDE, et les tests bouchonnent pgrep et pkill. --- EN --- PyCharm's closing net looked for "/opt/pycharm" in command lines. But the install script is passed to ssh as an argument, and it contains that path: the pkill killed the ssh session carrying a running install. It died with no exit marker, and the dashboard showed an hourglass for 54 minutes. Running the test suite was enough to trigger it, since one test replays the step. The net now targets process NAMES, scoped to the current account. Measured in the VM: by name, 3 real processes and no false ones; by command line, 4 — the ssh included. A test plants a witness named "sleep" whose command line holds the IDE path, and the tests stub pgrep and pkill. Assisted-by: Claude Opus 5 --- script/todo/todo.py | 28 ++++++---- test/test_qemu_desktop_tools.py | 92 ++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 11 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 9d2696b..2e9e709 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -5785,18 +5785,26 @@ class TODO: "for i in $(seq 1 12); do kill -0 -$pid 2>/dev/null || break; " "sleep 5; done; kill -KILL -$pid 2>/dev/null; " # Filet, et il a sa raison d'être : ce qui survit ici mange la - # mémoire de TOUTES les étapes suivantes. On le nomme au lieu de le - # laisser courir, et on ne vise que l'IDE de /opt et l'écran :99. - # « pgrep -fc » IMPRIME 0 et rend 1 quand il ne trouve rien : le - # « || echo 0 » ajoutait un second zéro, et « 0\n0 » n'est pas - # « 0 » — le filet se déclenchait donc toujours. « wc -l » rend un - # seul nombre et un code 0. - 'left=$(pgrep -f "[/]opt/pycharm|[X]vfb :99" 2>/dev/null ' - "| wc -l); " + # mémoire de TOUTES les étapes suivantes. + # + # Par NOM de processus (« -x »), jamais par ligne de commande. Un + # « pkill -f /opt/pycharm » attrape aussi le ssh QUI PORTE cette + # installation — sa ligne de commande contient le script entier, + # donc ce chemin. Vécu : une installation est morte en silence, sa + # session ssh emportée, 48 minutes perdues. Mesuré ensuite : par + # nom, 3 processus réels attrapés et 0 faux ; par ligne de commande, + # 4 dont le ssh. Les noms sont ceux relevés dans la VM — pycharm, + # Xvfb, fsnotifier, cef_server — et « -u » borne au compte courant. + # + # « pgrep -c » IMPRIME 0 et rend 1 quand il ne trouve rien : un + # « || echo 0 » donnerait « 0\n0 », qui n'est pas « 0 ». « wc -l » + # rend un seul nombre et un code 0. + 'left=$(pgrep -u "$(id -u)" -x ' + '"pycharm|cef_server|fsnotifier|Xvfb" 2>/dev/null | wc -l); ' '[ "$left" = 0 ] || { ' f'echo " {t("closing what survived the first open:")} $left"; ' - 'pkill -f "[/]opt/pycharm" 2>/dev/null; ' - 'pkill -f "[X]vfb :99" 2>/dev/null; sleep 2; }; ' + 'pkill -u "$(id -u)" -x ' + '"pycharm|cef_server|fsnotifier|Xvfb" 2>/dev/null; sleep 2; }; ' '[ "$ok" = 1 ]; fi; fi; } && ' f'echo " {t("project created, the install will configure it")}" ' f'|| echo " ⚠ {t("no .idea: open PyCharm once, then")} ' diff --git a/test/test_qemu_desktop_tools.py b/test/test_qemu_desktop_tools.py index 69a677d..35974ee 100644 --- a/test/test_qemu_desktop_tools.py +++ b/test/test_qemu_desktop_tools.py @@ -735,7 +735,21 @@ class TestPycharmFirstOpen(unittest.TestCase): ) (bin_dir / "sudo").write_text("#!/bin/bash\nexit 0\n") (bin_dir / "python3").write_text("#!/bin/bash\ncat > /dev/null\n") - for name in ("xvfb-run", "pycharm", "sudo", "python3"): + # pgrep et pkill sont BOUCHONNÉS, et c'est le point important : le + # filet de l'étape balaie les processus du compte courant. Exécuté + # sans bouchon sur la machine de développement, il fermerait le + # PyCharm de l'utilisateur. C'est le groupe qu'on teste ici, pas le + # filet — celui-ci est vérifié à part, sans rien tuer. + (bin_dir / "pgrep").write_text("#!/bin/bash\nexit 1\n") + (bin_dir / "pkill").write_text("#!/bin/bash\nexit 0\n") + for name in ( + "xvfb-run", + "pycharm", + "sudo", + "python3", + "pgrep", + "pkill", + ): (bin_dir / name).chmod(0o755) cmd = self.todo._qemu_pycharm_project_cmd(False).replace( self.todo._qemu_install_dir(False), str(repo) @@ -759,6 +773,82 @@ class TestPycharmFirstOpen(unittest.TestCase): ) +class TestPycharmNetIsNarrow(unittest.TestCase): + """Le filet de fermeture ne doit JAMAIS viser le ssh qui porte l'install. + + Vécu, et cher : le filet cherchait « /opt/pycharm » dans les LIGNES DE + COMMANDE. Or la commande d'installation est passée en argument à ssh, et + elle contient ce chemin — le pkill a donc tué la session ssh qui portait + l'installation en cours sur l'hyperviseur. Elle est morte en silence, sans + marqueur de sortie : 48 minutes perdues, et rien dans le journal. + + Mesuré ensuite dans une VM : par NOM de processus, 3 processus réels + (pycharm, Xvfb, fsnotifier) et aucun faux ; par ligne de commande, 4 — le + ssh compris. + """ + + def setUp(self): + self.todo = TODO.__new__(TODO) + self.cmd = self.todo._qemu_pycharm_project_cmd() + + def test_it_matches_by_process_name(self): + self.assertIn('pgrep -u "$(id -u)" -x', self.cmd) + self.assertIn('pkill -u "$(id -u)" -x', self.cmd) + + def test_it_never_matches_by_command_line(self): + """« -f » est exactement ce qui a tué l'installation.""" + self.assertNotIn("pkill -f", self.cmd) + self.assertNotIn("pgrep -f", self.cmd) + + def test_the_names_are_the_ones_measured_in_the_vm(self): + for name in ("pycharm", "cef_server", "fsnotifier", "Xvfb"): + self.assertIn(name, self.cmd) + + def test_a_command_line_that_merely_mentions_the_ide_is_spared(self): + """Le test qui compte, et il ne tue rien : un témoin dont la LIGNE + contient le chemin de l'IDE — comme le ssh lanceur — et dont le NOM est + « sleep ». L'ancien motif l'attrape, le nouveau l'épargne.""" + import os + import re + import time + + pattern = re.search(r'-x "([^"]+)"', self.cmd).group(1) + witness = subprocess.Popen( + [ + "bash", + "-c", + 'exec -a "ssh erplibre@vm bash -c /opt/pycharm/bin/pycharm.sh"' + " sleep 30", + ] + ) + try: + time.sleep(1.5) + uid = str(os.getuid()) + by_name = subprocess.run( + ["pgrep", "-u", uid, "-x", pattern], + capture_output=True, + text=True, + ).stdout.split() + by_cmdline = subprocess.run( + ["pgrep", "-u", uid, "-f", "[/]opt/pycharm"], + capture_output=True, + text=True, + ).stdout.split() + pid = str(witness.pid) + # Le témoin est un enfant de bash : on cherche le groupe entier. + spared = pid not in by_name + self.assertTrue(spared, "le motif par nom a attrapé le témoin") + self.assertIn( + pid, + by_cmdline, + "le témoin devrait être attrapé par l'ancien motif ;" + " sinon ce test ne prouve rien", + ) + finally: + witness.kill() + witness.wait(timeout=10) + + class TestMobileSwap(unittest.TestCase): """Le swap posé avant de compiler, et son refus de bloquer.