From 2355fed064b7980f697c55e0563eaf2cd755b13c Mon Sep 17 00:00:00 2001 From: Yann Beaucoud Date: Wed, 9 Sep 2026 22:14:44 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20ce=20que=20l'installateur=20pose=20attei?= =?UTF-8?q?nt=20enfin=20le=20syst=C3=A8me=20qui=20tourne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois remontées de testeurs le 09/09/2026, même famille : le choix est bien enregistré, mais personne ne le rejoue là où il compte. 1. ROTATION DU PLAYFIELD (Flo) pincabos-screen-hotplug est la SEULE unité qui applique playfield_rotation (xrandr --rotate, depuis screens.json). Elle n'était démarrée que par la règle udev « ACTION==change, SUBSYSTEM==drm » : au démarrage, écrans déjà branchés, les connecteurs sont énumérés en « add » et l'événement « change » ne vient jamais. Son .service n'avait pas de section [Install], donc aucune cible ne la démarrait. La rotation choisie à l'installation n'était jamais appliquée au reboot. L'ordre était déjà prévu pour le boot (After=pincabos-screen-topology-boot) : il ne manquait que le déclenchement. 2. ZeDMD (Flo) L'installateur appliquait la configuration ZeDMD depuis le chroot du média : pas de réseau final, pas de périphérique, et un « || true » qui avalait l'échec. Un ZeDMD en Wi-Fi ne pouvait pas y répondre. On ne pose plus que le drapeau — pincabos-dmd-installer.service rejoue au premier démarrage et CONSERVE le drapeau tant que l'application échoue, donc il retente aux démarrages suivants. 3. AUCUNE TABLE NE DÉMARRE (Patrick) mv: cannot move '/home/pinball/.local/share/VPinballX/10.8' to '/home/pinball/.pincabos/vpx': Permission denied Renommer un dossier demande le droit d'écriture sur le PARENT. Le dossier appartenait à root sur une installation neuve : le mv échouait, set -e tuait le lanceur, VPX ne démarrait pas. VPinFE se contentait de réafficher son menu, et le journal du lanceur montrait « Lancement Original direct » puis « frontend réactivé » à la même seconde. Deux corrections : le chown de l'installateur couvre maintenant /home/pinball (il s'arrêtait à /opt), et la migration des préférences ne peut plus tuer le lanceur — quand elle échoue, on continue avec l'ancien dossier, en le disant. Et le lanceur parle : seul le code 139 (SIGSEGV) était traité, tout autre échec repartait en silence. Un retour non nul en moins de cinq secondes est désormais journalisé avec son code. C'est ce qui manquait pour diagnostiquer. Au passage : test_dof_port_famille (livré en #279) remplaçait glob.glob dans TOUT le processus au lieu de l'attribut du module. Deux tests sans rapport échouaient à retardement en recevant des listes vides. 723 tests. Co-Authored-By: Claude Opus 5 --- .../pincabos-screen-hotplug.service | 1 + .../system/pincabos-screen-hotplug.service | 12 ++ .../script/installer/pincabos-live-installer | 15 ++- .../scripts/VPXlauncher.pincabos-original.sh | 26 +++- opt/pincabos/scripts/VPXlauncher.real.sh | 11 ++ opt/pincabos/tests/test_dof_port_famille.py | 24 +++- .../tests/test_installation_atteint.py | 127 ++++++++++++++++++ opt/pincabos/tests/test_installer_dmd.py | 9 +- 8 files changed, 208 insertions(+), 17 deletions(-) create mode 120000 etc/systemd/system/graphical.target.wants/pincabos-screen-hotplug.service create mode 100755 opt/pincabos/tests/test_installation_atteint.py diff --git a/etc/systemd/system/graphical.target.wants/pincabos-screen-hotplug.service b/etc/systemd/system/graphical.target.wants/pincabos-screen-hotplug.service new file mode 120000 index 00000000..a4cabac5 --- /dev/null +++ b/etc/systemd/system/graphical.target.wants/pincabos-screen-hotplug.service @@ -0,0 +1 @@ +../pincabos-screen-hotplug.service \ No newline at end of file diff --git a/etc/systemd/system/pincabos-screen-hotplug.service b/etc/systemd/system/pincabos-screen-hotplug.service index f372b3fd..d9c367d5 100644 --- a/etc/systemd/system/pincabos-screen-hotplug.service +++ b/etc/systemd/system/pincabos-screen-hotplug.service @@ -9,3 +9,15 @@ StartLimitBurst=3 [Service] Type=oneshot ExecStart=/usr/local/libexec/pincabos/pincabos-screen-hotplug + +[Install] +# PINCABOS_ROTATION_AU_BOOT_V1 : cette unite est la SEULE a appliquer +# playfield_rotation (xrandr --rotate, depuis screens.json). Elle n etait +# demarree que par la regle udev « ACTION==change, SUBSYSTEM==drm » : sur un +# demarrage normal, ecrans deja branches, les connecteurs sont enumeres en +# « add » et l evenement « change » ne vient jamais. La rotation choisie dans +# l installateur n etait donc jamais appliquee au reboot (remonte par Flo le +# 09/09/2026 : playfield tete en bas, corrige a la main depuis la WebApp). +# L ordre est deja prevu pour le boot (After=pincabos-screen-topology-boot, +# PINCABOS_TOPOLOGIE_VERROU_BOOT_V1) : il ne manquait que le declenchement. +WantedBy=graphical.target diff --git a/opt/pincabos/script/installer/pincabos-live-installer b/opt/pincabos/script/installer/pincabos-live-installer index 5e3aaf80..136bdf4d 100755 --- a/opt/pincabos/script/installer/pincabos-live-installer +++ b/opt/pincabos/script/installer/pincabos-live-installer @@ -1097,9 +1097,13 @@ apply_target_dmd() { install -o 1000 -g 1000 -m 0664 "$src" "$TARGET/opt/pincabos/config/zedmd.json" install -d -m 0755 "$TARGET/opt/pincabos/flags" date -Is > "$TARGET/opt/pincabos/flags/dmd-installer.pending" - if [ -x "$TARGET/opt/pincabos/tools/pincabos-zedmd" ]; then - chroot "$TARGET" runuser -u pinball -- /opt/pincabos/tools/pincabos-zedmd apply 2>&1 | sed 's/^/ /' || true - fi + # PINCABOS_ZEDMD_AU_PREMIER_DEMARRAGE_V1 : on n applique plus rien depuis le + # chroot. Un ZeDMD en Wi-Fi n y est pas joignable — pas de reseau final, pas + # de peripherique — et le « || true » avalait l echec en silence (remonte par + # Flo le 09/09/2026 : IP renseignee a l installation, ZeDMD inactif ensuite). + # Le drapeau ci-dessus suffit : pincabos-dmd-installer.service rejoue + # l application au premier demarrage et CONSERVE le drapeau tant qu elle + # echoue, donc il retente aux demarrages suivants. pco_go "zedmd.json installed (mode: $(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("mode","off"))' "$src" 2>/dev/null || echo '?'))" } @@ -1240,6 +1244,11 @@ apply_target_ownership() { /opt/pincabos/config /opt/pincabos/logs /opt/pincabos/backups \ /opt/pincabos/state /opt/pincabos/download /opt/pincabos/tmp \ /opt/pincabos/uploads /opt/pincabos/cache /opt/pinball 2>/dev/null || true + # PINCABOS_CIBLE_DROITS_V2 : le compte du joueur aussi. Remonte par Patrick le + # 09/09/2026 : ~/.local/share/VPinballX appartenait a root, le lanceur VPX + # echouait a migrer le dossier de preferences (« mv : Permission denied ») et + # mourait sur son set -e. Aucune table ne demarrait, sans un mot d erreur. + chroot "$TARGET" chown -R pinball:pinball /home/pinball 2>/dev/null || true pco_go "Droits du compte joueur appliques" } diff --git a/opt/pincabos/scripts/VPXlauncher.pincabos-original.sh b/opt/pincabos/scripts/VPXlauncher.pincabos-original.sh index e43bc0b7..03bc5cbb 100755 --- a/opt/pincabos/scripts/VPXlauncher.pincabos-original.sh +++ b/opt/pincabos/scripts/VPXlauncher.pincabos-original.sh @@ -83,17 +83,31 @@ fi # Un symlink de compatibilité conserve l ancien chemin pour tous les lecteurs. VPX_PREF_DIR="$PCO_VPX_PREF" VPX_LEGACY_PREF="$PCO_VPX_LEGACY_PREF" +# PINCABOS_VPX_PREFPATH_V2 : la migration ne doit JAMAIS empecher de jouer. +# Remonte par Patrick le 09/09/2026, sur une installation neuve : +# mv: cannot move '/home/pinball/.local/share/VPinballX/10.8' +# to '/home/pinball/.pincabos/vpx': Permission denied +# Le dossier appartenait a root ; renommer demande le droit d ecriture sur le +# PARENT. Le mv echouait, `set -e` tuait le lanceur, VPX ne demarrait pas — et +# rien ne le disait : VPinFE se contentait de reafficher le menu. +# Les droits sont corriges a l installation (PINCABOS_CIBLE_DROITS_V2), mais le +# lanceur doit rester jouable meme quand la migration est impossible : on garde +# alors l ancien dossier, en le disant. if [[ ! -e "${VPX_PREF_DIR}" ]]; then - mkdir -p "$(dirname "${VPX_PREF_DIR}")" + mkdir -p "$(dirname "${VPX_PREF_DIR}")" 2>/dev/null || true if [[ -d "${VPX_LEGACY_PREF}" && ! -L "${VPX_LEGACY_PREF}" ]]; then - mv "${VPX_LEGACY_PREF}" "${VPX_PREF_DIR}" - else - mkdir -p "${VPX_PREF_DIR}" + if ! mv "${VPX_LEGACY_PREF}" "${VPX_PREF_DIR}" 2>/dev/null; then + echo "PINCABOS [PREFS] Migration impossible (droits ?) : on continue avec ${VPX_LEGACY_PREF}." >&2 + VPX_PREF_DIR="${VPX_LEGACY_PREF}" + fi + elif ! mkdir -p "${VPX_PREF_DIR}" 2>/dev/null; then + echo "PINCABOS [PREFS] ${VPX_PREF_DIR} increable : on continue avec ${VPX_LEGACY_PREF}." >&2 + VPX_PREF_DIR="${VPX_LEGACY_PREF}" fi fi if [[ ! -e "${VPX_LEGACY_PREF}" ]]; then - mkdir -p "$(dirname "${VPX_LEGACY_PREF}")" - ln -sn "${VPX_PREF_DIR}" "${VPX_LEGACY_PREF}" + mkdir -p "$(dirname "${VPX_LEGACY_PREF}")" 2>/dev/null || true + ln -sn "${VPX_PREF_DIR}" "${VPX_LEGACY_PREF}" 2>/dev/null || true fi chown -h "$PINBALL_USER:$PINBALL_USER" "${VPX_LEGACY_PREF}" 2>/dev/null || true chown "$PINBALL_USER:$PINBALL_USER" "$(dirname "${VPX_PREF_DIR}")" "${VPX_PREF_DIR}" 2>/dev/null || true diff --git a/opt/pincabos/scripts/VPXlauncher.real.sh b/opt/pincabos/scripts/VPXlauncher.real.sh index 36f52786..e419ae4c 100755 --- a/opt/pincabos/scripts/VPXlauncher.real.sh +++ b/opt/pincabos/scripts/VPXlauncher.real.sh @@ -32,6 +32,7 @@ run_with_front_windows() { "$PLACER" --place >/dev/null 2>&1 & fi local rc=0 + local DEBUT=$SECONDS set +e "$REAL" "$@" rc=$? @@ -39,6 +40,16 @@ run_with_front_windows() { if [[ -x "$PLACER" ]]; then "$PLACER" --restore >/dev/null 2>&1 || true fi + # PINCABOS_LANCEUR_SORTIE_PARLANTE_V1 : seul le code 139 (SIGSEGV) etait + # traite ; tout autre echec repartait en silence. Le 09/09/2026, VPX rendait + # la main en moins d une seconde chez Patrick et le journal du lanceur ne + # montrait que « Lancement Original direct » puis « frontend reactive » a la + # meme seconde — aucune trace de la cause. Un retour immediat et non nul est + # anormal : on le dit, avec le code. + if [[ "$rc" -ne 0 ]] && (( SECONDS - DEBUT < 5 )); then + echo "PINCABOS [LANCEUR] VPX a rendu la main en $(( SECONDS - DEBUT ))s avec le code $rc." >&2 + echo "PINCABOS [LANCEUR] Table : ${TABLE:-inconnue}" >&2 + fi # PINCABOS_VULKAN_SEGFAULT_FALLBACK_V1 : 139 = tue par le signal 11 if [[ "$rc" -eq 139 && -n "${TABLE:-}" && "${PINCABOS_BACKEND_RETRY:-0}" != "1" && -x "$BACKEND_FALLBACK" ]]; then if "$BACKEND_FALLBACK" "$TABLE"; then diff --git a/opt/pincabos/tests/test_dof_port_famille.py b/opt/pincabos/tests/test_dof_port_famille.py index 4795469b..810ad651 100755 --- a/opt/pincabos/tests/test_dof_port_famille.py +++ b/opt/pincabos/tests/test_dof_port_famille.py @@ -19,6 +19,7 @@ partager la meme recherche. """ import importlib.util +import types import unittest from pathlib import Path @@ -45,12 +46,23 @@ def _module(): } +def _poser_bus(m, bus): + """Fait croire au module qu il voit ce bus USB. + + On remplace l ATTRIBUT `glob` du module par un objet a nous. Surtout pas + `m.glob.glob = ...` : `m.glob` est le module glob lui-meme, partage par tout + le processus — le remplacer casse silencieusement tous les tests suivants qui + listent des fichiers (deux echecs a retardement, 09/09/2026). + """ + m._udev = lambda dev: bus.get(dev, {}) + m.glob = types.SimpleNamespace( + glob=lambda motif: sorted(d for d in bus if d.startswith(motif.rstrip("*")))) + + class PortsDistincts(unittest.TestCase): def setUp(self): self.m = _module() - self.m._udev = lambda dev: FAUX_BUS.get(dev, {}) - self.m.glob.glob = lambda motif: sorted( - d for d in FAUX_BUS if d.startswith(motif.rstrip("*"))) + _poser_bus(self.m, FAUX_BUS) def test_la_wemos_ne_prend_pas_le_port_de_la_teensy(self): strips = [{"controller": "TeensyStripController", "name": "Teensy 1"}, @@ -68,7 +80,7 @@ def test_l_ordre_de_declaration_ne_change_rien(self): self.assertEqual(ports, ["/dev/ttyUSB0", "/dev/ttyACM1"]) def test_sans_materiel_le_repli_reste_dans_la_bonne_famille(self): - self.m.glob.glob = lambda motif: [] + _poser_bus(self.m, {}) strips = [{"controller": "WemosD1MPStripController", "name": "Wemos 1"}, {"controller": "TeensyStripController", "name": "Teensy 2"}] ports = [r["com_port"] for r in self.m._resoudre_ports(strips)] @@ -84,9 +96,7 @@ def test_deux_cartes_de_la_meme_famille_ne_se_marchent_pas_dessus(self): bus = dict(FAUX_BUS) bus["/dev/ttyUSB1"] = {"ID_VENDOR_ID": "10c4", "ID_SERIAL": "cp210x", "ID_MODEL": "CP2102", "ID_SERIAL_SHORT": ""} - self.m._udev = lambda dev: bus.get(dev, {}) - self.m.glob.glob = lambda motif: sorted( - d for d in bus if d.startswith(motif.rstrip("*"))) + _poser_bus(self.m, bus) strips = [{"controller": "WemosD1MPStripController", "name": "W1"}, {"controller": "WemosD1MPStripController", "name": "W2"}] ports = [r["com_port"] for r in self.m._resoudre_ports(strips)] diff --git a/opt/pincabos/tests/test_installation_atteint.py b/opt/pincabos/tests/test_installation_atteint.py new file mode 100755 index 00000000..4b6ed3b5 --- /dev/null +++ b/opt/pincabos/tests/test_installation_atteint.py @@ -0,0 +1,127 @@ +"""Ce que l'installateur pose doit atteindre le systeme qui tourne. + +Trois remontees du 09/09/2026, meme famille : + + Flo « Reglage ecran ok pendant l'installation mais apres reboot sur le dd + ca n'a pas suivi » — playfield tete en bas, refait a la main depuis + la WebApp. + Flo « J'ai fait la config IP de mon zedmd pendant l'installation mais il + n'est pas actif sous vpinfe. » + Patrick « Ca semble vouloir se lancer, l'affichage s'estompe, une animation de + chargement, et ca revient sur l'interface. » + +Trois causes distinctes, toutes du meme genre : le choix est bien enregistre, +mais personne ne le rejoue la ou il compte. +""" +import re +import unittest +from pathlib import Path + +from _charge import RACINE + +R = Path(RACINE) +HOTPLUG = R / "etc/systemd/system/pincabos-screen-hotplug.service" +LIEN = R / "etc/systemd/system/graphical.target.wants/pincabos-screen-hotplug.service" +INSTALLEUR = R / "opt/pincabos/script/installer/pincabos-live-installer" +LANCEUR = R / "opt/pincabos/scripts/VPXlauncher.pincabos-original.sh" +REEL = R / "opt/pincabos/scripts/VPXlauncher.real.sh" + + +class RotationAppliqueeAuDemarrage(unittest.TestCase): + """La rotation du playfield vient de screens.json et n est posee que par + xrandr, dans pincabos-screen-hotplug. Cette unite n etait demarree que par + une regle udev « change » sur drm — qui ne survient pas quand les ecrans + sont deja branches au demarrage.""" + + def test_l_unite_est_demarree_au_boot(self): + s = HOTPLUG.read_text(encoding="utf-8") + self.assertIn("[Install]", s, "sans [Install], aucune cible ne la demarre") + self.assertRegex(s, r"(?m)^WantedBy=graphical\.target$") + + def test_le_lien_d_activation_existe(self): + self.assertTrue(LIEN.is_symlink(), "le lien graphical.target.wants manque") + + def test_l_ordre_du_boot_est_conserve(self): + # PINCABOS_TOPOLOGIE_VERROU_BOOT_V1 : la topologie de demarrage passe + # d abord, sinon le verrou bloquait le frontend (regression de la 4.32). + s = HOTPLUG.read_text(encoding="utf-8") + self.assertRegex(s, r"(?m)^After=.*pincabos-screen-topology-boot\.service") + + def test_c_est_toujours_le_hotplug_qui_tourne_les_ecrans(self): + # si un jour la rotation demenage, ce test doit etre revu sciemment + code = (R / "usr/local/libexec/pincabos/pincabos-screen-hotplug").read_text(encoding="utf-8") + self.assertIn("--rotate", code) + self.assertIn("screens.json", code) + + +class ZedmdRejoueLaOuIlEstJoignable(unittest.TestCase): + """Un ZeDMD en Wi-Fi n est pas joignable depuis le chroot du media : pas de + reseau final, pas de peripherique. L application appartient au premier + demarrage du cabinet.""" + + def test_plus_d_application_dans_le_chroot(self): + s = INSTALLEUR.read_text(encoding="utf-8") + self.assertNotIn("pincabos-zedmd apply", s, + "l installateur ne doit plus appliquer le ZeDMD depuis le chroot") + + def test_le_drapeau_de_rejeu_reste_pose(self): + # c est lui qui declenche pincabos-dmd-installer.service au boot, et ce + # service CONSERVE le drapeau tant que l application echoue : il retente. + s = INSTALLEUR.read_text(encoding="utf-8") + self.assertIn("flags/dmd-installer.pending", s) + + def test_le_service_de_rejeu_est_active(self): + self.assertTrue((R / "etc/systemd/system/multi-user.target.wants" + / "pincabos-dmd-installer.service").exists()) + + +class LeCompteDuJoueurLuiAppartient(unittest.TestCase): + """PINCABOS_CIBLE_DROITS_V2 — le chown s arretait a /opt.""" + + def test_home_pinball_est_dans_le_chown(self): + corps = INSTALLEUR.read_text(encoding="utf-8") \ + .split("apply_target_ownership() {", 1)[1].split("\n}", 1)[0] + self.assertRegex(corps, r"chown -R pinball:pinball /home/pinball\b", + "sans lui, VPX ne peut pas migrer son dossier de preferences") + + +class LaMigrationDesPrefsNeTuePasLeLanceur(unittest.TestCase): + """Chez Patrick : + + mv: cannot move '/home/pinball/.local/share/VPinballX/10.8' + to '/home/pinball/.pincabos/vpx': Permission denied + + Renommer un dossier demande le droit d ecriture sur le PARENT. Le mv + echouait, `set -Eeuo pipefail` tuait le lanceur, VPX ne demarrait pas — et + VPinFE se contentait de reafficher son menu.""" + + def test_le_mv_est_garde(self): + s = LANCEUR.read_text(encoding="utf-8") + bloc = s.split("PINCABOS_VPX_PREFPATH_V2", 1)[1][:1500] + self.assertRegex(bloc, r'if ! mv "\$\{VPX_LEGACY_PREF\}" "\$\{VPX_PREF_DIR\}"', + "le mv doit etre teste, pas laisse tuer le script") + + def test_le_repli_garde_l_ancien_dossier(self): + bloc = LANCEUR.read_text(encoding="utf-8").split("PINCABOS_VPX_PREFPATH_V2", 1)[1][:1500] + self.assertIn('VPX_PREF_DIR="${VPX_LEGACY_PREF}"', bloc, + "quand la migration echoue, on doit continuer avec l ancien dossier") + + def test_le_set_e_est_toujours_la(self): + # on ne corrige pas en desarmant le garde-fou du script entier + self.assertRegex(LANCEUR.read_text(encoding="utf-8"), r"(?m)^set -Eeuo pipefail$") + + +class LeLanceurDitPourquoiVpxEstParti(unittest.TestCase): + """Seul le code 139 (SIGSEGV) etait traite. Un retour immediat et non nul + repartait en silence : le journal ne montrait que « Lancement Original + direct » puis « frontend reactive », a la meme seconde.""" + + def test_une_sortie_rapide_et_non_nulle_est_journalisee(self): + s = REEL.read_text(encoding="utf-8") + self.assertIn("PINCABOS_LANCEUR_SORTIE_PARLANTE_V1", s) + self.assertRegex(s, r'\$rc" -ne 0') + self.assertIn("SECONDS - DEBUT", s) + + def test_le_repli_opengl_sur_139_est_conserve(self): + s = REEL.read_text(encoding="utf-8") + self.assertIn('"$rc" -eq 139', s, "le repli OpenGL ne doit pas disparaitre") diff --git a/opt/pincabos/tests/test_installer_dmd.py b/opt/pincabos/tests/test_installer_dmd.py index 9091482e..06376b96 100644 --- a/opt/pincabos/tests/test_installer_dmd.py +++ b/opt/pincabos/tests/test_installer_dmd.py @@ -167,7 +167,14 @@ def test_iso_sh(self): self.assertIn(" apply_target_network\n apply_target_dmd\n", s) self.assertIn('"$TARGET/opt/pincabos/config/zedmd.json"', s) self.assertIn("dmd-installer.pending", s) - self.assertIn("runuser -u pinball -- /opt/pincabos/tools/pincabos-zedmd apply", s) + # PINCABOS_ZEDMD_AU_PREMIER_DEMARRAGE_V1 : l installateur n applique plus + # rien depuis le chroot. Un ZeDMD en Wi-Fi n y est pas joignable — pas de + # reseau final, pas de peripherique — et le « || true » avalait l echec + # (remonte par Flo le 09/09/2026 : IP renseignee, ZeDMD inactif ensuite). + # Le drapeau suffit : pincabos-dmd-installer.service rejoue au premier + # demarrage et CONSERVE le drapeau tant que l application echoue. + self.assertNotIn("pincabos-zedmd apply", s, + "plus d application ZeDMD depuis le chroot du media") def test_premier_demarrage(self): u = (R / "etc/systemd/system/pincabos-dmd-installer.service").read_text(encoding="utf-8")