Cinq secondes ne suffisaient pas à LIRE la question. Le compte à rebours n'existe pas pour aller vite mais pour qu'on puisse s'absenter ; trop court, il fait l'inverse — on répond par réflexe, ou l'on subit un défaut non lu. Et « test » ne disait rien de ce qu'on migrait : des migrations successives portaient toutes le même nom, quand le fichier de sauvegarde en porte déjà un parlant. Le nom est assaini — il finit dans un createdb — et borné à 41 caractères : le pilote ajoute « _neutralize_upgrade_18 » et PostgreSQL tronque à 63, ce qui ferait finir deux paliers sur le même nom. Un téléchargement distant garde celui que le serveur a donné. --- EN --- Five seconds were not enough to READ the question. The countdown does not exist to go fast but so you can step away; too short, it does the opposite — you answer by reflex, or you get a default you never read. And "test" said nothing about what was being migrated: successive migrations all carried the same name, when the backup file already carries a telling one. The name is sanitised — it ends up in a createdb — and capped at 41 characters: the driver appends "_neutralize_upgrade_18" and PostgreSQL truncates at 63, which would land two steps on one name. A remote download keeps the name the server gave. Assisted-by: Claude Opus 5 (cherry picked from commit dad0f6b0ef4551898fe2964e177ff5c1c888b5dc)
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
"""Lire une réponse, ou prendre le défaut au bout du délai.
|
|
|
|
Le mode auto de la migration ne vaut que si TOUTES les invites le
|
|
connaissent. Or six d'entre elles vivent dans des outils lancés en
|
|
sous-processus — le désinstalleur de thème, le détecteur de SCSS figé, le
|
|
test de fumée. Elles ne partagent ni l'objet du pilote ni sa mémoire ; sans
|
|
rien de commun, elles attendraient indéfiniment une frappe qui ne vient
|
|
pas, et l'automatisation s'arrêterait là sans rien dire.
|
|
|
|
Ce qui traverse un `fork`, c'est l'environnement. Le pilote y pose
|
|
`ERPLIBRE_AUTO_EXECUTE`, chaque outil le lit, et il n'existe qu'une seule
|
|
implémentation du compte à rebours.
|
|
|
|
`select` plutôt qu'un fil ou une alarme : la question est « quelque chose
|
|
est-il LISIBLE maintenant », et il faut pouvoir renoncer. Un fil laisserait
|
|
derrière lui un `input()` toujours bloqué, qui volerait la frappe suivante.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
sys.path.append(
|
|
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
)
|
|
|
|
try:
|
|
from script.todo.todo_i18n import t
|
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
|
|
|
def t(key: str) -> str:
|
|
return key
|
|
|
|
|
|
ENV_ENABLED = "ERPLIBRE_AUTO_EXECUTE"
|
|
ENV_DELAY = "ERPLIBRE_AUTO_DELAY"
|
|
DEFAULT_DELAY = 15
|
|
|
|
|
|
def enabled():
|
|
"""Le mode auto est-il actif pour CE processus ?"""
|
|
return os.environ.get(ENV_ENABLED, "") == "1"
|
|
|
|
|
|
def delay():
|
|
"""Combien de secondes attendre. Une valeur illisible ne bloque rien."""
|
|
try:
|
|
value = float(os.environ.get(ENV_DELAY, DEFAULT_DELAY))
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_DELAY
|
|
# Un délai nul ou négatif rendrait la reprise en main impossible : on
|
|
# ne peut pas interrompre ce qui ne laisse aucune fenêtre.
|
|
return value if value > 0 else DEFAULT_DELAY
|
|
|
|
|
|
def export(on, seconds=None):
|
|
"""Poser le mode auto dans l'environnement, pour tout ce qu'on lancera.
|
|
|
|
C'est le seul canal qu'un sous-processus partage avec nous. Le retirer
|
|
quand on l'éteint, plutôt que d'y écrire « 0 », évite qu'un reste de
|
|
session précédente décide à notre place.
|
|
"""
|
|
if on:
|
|
os.environ[ENV_ENABLED] = "1"
|
|
os.environ[ENV_DELAY] = str(
|
|
seconds if seconds is not None else DEFAULT_DELAY
|
|
)
|
|
else:
|
|
os.environ.pop(ENV_ENABLED, None)
|
|
os.environ.pop(ENV_DELAY, None)
|
|
|
|
|
|
def ask(prompt, default="", seconds=None):
|
|
"""Poser la question. Rendre `default` si rien n'arrive à temps.
|
|
|
|
Hors mode auto, c'est un `input()` ordinaire — sauf qu'une réponse vide
|
|
vaut le défaut, comme partout ailleurs en ligne de commande. C'est ce
|
|
qui permet d'écrire l'attendu dans la question : « (Y/n) » doit dire la
|
|
vérité pour la personne qui appuie sur Entrée, pas seulement pour le
|
|
compte à rebours.
|
|
"""
|
|
if not enabled():
|
|
return input(prompt) or default
|
|
import select
|
|
|
|
attente = delay() if seconds is None else seconds
|
|
# DIRE qu'on répondra tout seul, et dans combien de temps. Une invite
|
|
# qui a l'air d'attendre indéfiniment n'invite pas à s'absenter, et
|
|
# c'est pourtant tout l'intérêt du mode auto. Devant l'invite plutôt
|
|
# qu'après : elle se lit avant qu'on décide de partir.
|
|
sys.stdout.write(f"⏱{attente:g}s {prompt}")
|
|
sys.stdout.flush()
|
|
try:
|
|
ready, _, _ = select.select([sys.stdin], [], [], attente)
|
|
except (OSError, ValueError):
|
|
# stdin n'est pas sélectionnable : on ne devine pas, on demande.
|
|
return input("") or default
|
|
if ready:
|
|
answer = sys.stdin.readline().rstrip("\n")
|
|
# Une réponse VIDE vaut « prends le défaut » : c'est tout le propos
|
|
# du mode auto. Un stdin fermé — exécution non interactive — est
|
|
# lisible tout de suite et rend justement une ligne vide ; sans
|
|
# ceci, le défaut ne servirait jamais là.
|
|
return answer or default
|
|
# « Entrée », parce que c'est ce que l'invite a proposé. Elle annonce
|
|
# « Entrée = effacer, k = garder » et ne mentionne nulle part « d » :
|
|
# afficher la valeur brute nommait donc une réponse qui n'était pas au
|
|
# menu, et l'on se demandait ce que l'outil venait de décider. La
|
|
# valeur reste, entre parenthèses, pour qui veut la précision.
|
|
detail = f" ({default})" if default else ""
|
|
print(f" ⏱ → {t('Enter')}{detail}")
|
|
return default
|
|
|
|
|
|
def make_ask(default, seconds=None):
|
|
"""Un `ask` à un seul argument, pour les outils qui en injectent un.
|
|
|
|
Ils appellent `ask(question)` sans savoir ce qu'est un défaut : celui-ci
|
|
appartient à l'outil, pas à l'appelant, et se fige donc ici.
|
|
"""
|
|
|
|
def ask_one(prompt=""):
|
|
return ask(prompt, default=default, seconds=seconds)
|
|
|
|
return ask_one
|