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")