[FIX] script execute : afficher l'invite sans attendre le saut de ligne
« readline » ne rend la main qu'au saut de ligne : une invite qui n'en porte pas restait retenue jusqu'à la ligne suivante, écrite après la réponse. Toute question « [o/N] » d'une commande du menu se voyait donc après coup, et l'on y répondait à l'aveugle. La lecture se fait par blocs, le reliquat part à l'écran aussitôt et stdout est vidé ; un décodeur incrémental protège les accents coupés entre deux lectures. Vérifié : invite visible avant la frappe, sans double affichage ; 13 cas de non-régression ; 4546 tests, seuls les 9 échecs préexistants de qemu_proxmox. --- EN --- `readline` only returns at a newline: a prompt carrying none stayed held back until the next line, written after the answer. Every "[o/N]" question asked by a menu command was therefore seen after the fact, and answered blind. Reading now happens in chunks, the trailing fragment reaches the screen at once and stdout is flushed; an incremental decoder protects accented characters split across two reads. Checked: prompt visible before typing, with no double display; 13 regression cases; 4546 tests, only the 9 pre-existing qemu_proxmox failures. Assisted-by: Claude Opus 5
This commit is contained in:
parent
b8361c30e6
commit
af38810da5
1 changed files with 62 additions and 15 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
# migration meurt sur un TypeError avant d'avoir rien fait.
|
# migration meurt sur un TypeError avant d'avoir rien fait.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import codecs
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -183,31 +184,43 @@ class Execute:
|
||||||
executable="/bin/bash",
|
executable="/bin/bash",
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
# Octets bruts, SANS tampon. « readline » attendait le saut de
|
||||||
bufsize=1, # Disable buffering for live output
|
# ligne pour rendre la main : une invite qui n'en porte pas —
|
||||||
universal_newlines=True, # Handle line breaks correctly
|
# « Continuer ? [o/N] » — restait donc invisible jusqu'à ce que
|
||||||
|
# la réponse soit déjà tapée. La question s'affichait APRÈS la
|
||||||
|
# réponse, et l'on répondait à l'aveugle.
|
||||||
|
bufsize=0,
|
||||||
env=my_env,
|
env=my_env,
|
||||||
)
|
)
|
||||||
|
|
||||||
sink = getattr(self, "log_sink", None)
|
sink = getattr(self, "log_sink", None)
|
||||||
while True:
|
# Le tube porte des octets, et une lecture peut couper un caractère
|
||||||
line = process.stdout.readline()
|
# accentué ou un emoji en deux. Le décodeur incrémental garde le
|
||||||
if not line:
|
# morceau incomplet en attente au lieu de rendre un « ? ».
|
||||||
break
|
decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
||||||
# La sortie du sous-processus passe par le meme filtre que
|
fd = process.stdout.fileno()
|
||||||
# la commande : un outil qui reaffiche ses propres arguments
|
# « pending » est la ligne en cours, pas encore terminée ; « shown »
|
||||||
|
# compte ce qui en a déjà été envoyé au terminal, pour ne jamais
|
||||||
|
# afficher deux fois le même morceau d'invite quand la ligne finit
|
||||||
|
# par se terminer.
|
||||||
|
pending = ""
|
||||||
|
shown = 0
|
||||||
|
|
||||||
|
def retenir(ligne):
|
||||||
|
"""Journaliser et retenir une ligne complète, caviardée."""
|
||||||
|
nonlocal sink
|
||||||
|
# La sortie du sous-processus passe par le MÊME filtre que la
|
||||||
|
# commande : un outil qui réaffiche ses propres arguments
|
||||||
# (« set -x », une trace, odoo_bin.sh) y remettrait le secret
|
# (« set -x », une trace, odoo_bin.sh) y remettrait le secret
|
||||||
# que la ligne 165 venait d'ecarter.
|
# que l'affichage de la commande venait d'écarter.
|
||||||
line = redact_secrets(line)
|
clean = redact_secrets(ligne)
|
||||||
if not quiet:
|
|
||||||
print(line, end="")
|
|
||||||
if sink:
|
if sink:
|
||||||
# Chaque ligne passe DÉJÀ ici : c'est le seul endroit où
|
# Chaque ligne passe DÉJÀ ici : c'est le seul endroit où
|
||||||
# journaliser sans rien changer à ce que le terminal
|
# journaliser sans rien changer à ce que le terminal
|
||||||
# montre. Une erreur d'écriture ne doit jamais faire
|
# montre. Une erreur d'écriture ne doit jamais faire
|
||||||
# échouer la commande qu'on est en train de suivre.
|
# échouer la commande qu'on est en train de suivre.
|
||||||
try:
|
try:
|
||||||
sink.write(line)
|
sink.write(clean)
|
||||||
except Exception:
|
except Exception:
|
||||||
sink = None
|
sink = None
|
||||||
if (
|
if (
|
||||||
|
|
@ -216,11 +229,45 @@ class Execute:
|
||||||
):
|
):
|
||||||
# Remove last \n char
|
# Remove last \n char
|
||||||
output_lines.append(
|
output_lines.append(
|
||||||
line.removesuffix("\r\n")
|
clean.removesuffix("\r\n")
|
||||||
.removesuffix("\n")
|
.removesuffix("\n")
|
||||||
.removesuffix("\r")
|
.removesuffix("\r")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
chunk = os.read(fd, 65536)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
pending += decoder.decode(chunk)
|
||||||
|
while True:
|
||||||
|
coupe = pending.find("\n")
|
||||||
|
if coupe < 0:
|
||||||
|
break
|
||||||
|
ligne = pending[: coupe + 1]
|
||||||
|
pending = pending[coupe + 1 :]
|
||||||
|
if not quiet:
|
||||||
|
print(redact_secrets(ligne[shown:]), end="")
|
||||||
|
shown = 0
|
||||||
|
retenir(ligne)
|
||||||
|
if not quiet:
|
||||||
|
if len(pending) > shown:
|
||||||
|
# Le reliquat sans saut de ligne EST l'invite : la
|
||||||
|
# montrer tout de suite, avant que la commande ne se
|
||||||
|
# bloque sur la lecture de la réponse.
|
||||||
|
print(redact_secrets(pending[shown:]), end="")
|
||||||
|
shown = len(pending)
|
||||||
|
# Sans vidage explicite, cette invite resterait dans le
|
||||||
|
# tampon de Python : second endroit où la question se
|
||||||
|
# perdait, la sortie n'étant vidée qu'au saut de ligne.
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
pending += decoder.decode(b"", True)
|
||||||
|
if pending:
|
||||||
|
if not quiet:
|
||||||
|
print(redact_secrets(pending[shown:]), end="")
|
||||||
|
sys.stdout.flush()
|
||||||
|
retenir(pending)
|
||||||
|
|
||||||
process.wait()
|
process.wait()
|
||||||
exit_code = process.returncode
|
exit_code = process.returncode
|
||||||
if process.returncode != 0 and not quiet:
|
if process.returncode != 0 and not quiet:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue