[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.
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -183,31 +184,43 @@ class Execute:
|
|||
executable="/bin/bash",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1, # Disable buffering for live output
|
||||
universal_newlines=True, # Handle line breaks correctly
|
||||
# Octets bruts, SANS tampon. « readline » attendait le saut de
|
||||
# ligne pour rendre la main : une invite qui n'en porte pas —
|
||||
# « 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,
|
||||
)
|
||||
|
||||
sink = getattr(self, "log_sink", None)
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
# La sortie du sous-processus passe par le meme filtre que
|
||||
# la commande : un outil qui reaffiche ses propres arguments
|
||||
# Le tube porte des octets, et une lecture peut couper un caractère
|
||||
# accentué ou un emoji en deux. Le décodeur incrémental garde le
|
||||
# morceau incomplet en attente au lieu de rendre un « ? ».
|
||||
decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
||||
fd = process.stdout.fileno()
|
||||
# « 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
|
||||
# que la ligne 165 venait d'ecarter.
|
||||
line = redact_secrets(line)
|
||||
if not quiet:
|
||||
print(line, end="")
|
||||
# que l'affichage de la commande venait d'écarter.
|
||||
clean = redact_secrets(ligne)
|
||||
if sink:
|
||||
# Chaque ligne passe DÉJÀ ici : c'est le seul endroit où
|
||||
# journaliser sans rien changer à ce que le terminal
|
||||
# montre. Une erreur d'écriture ne doit jamais faire
|
||||
# échouer la commande qu'on est en train de suivre.
|
||||
try:
|
||||
sink.write(line)
|
||||
sink.write(clean)
|
||||
except Exception:
|
||||
sink = None
|
||||
if (
|
||||
|
|
@ -216,11 +229,45 @@ class Execute:
|
|||
):
|
||||
# Remove last \n char
|
||||
output_lines.append(
|
||||
line.removesuffix("\r\n")
|
||||
clean.removesuffix("\r\n")
|
||||
.removesuffix("\n")
|
||||
.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()
|
||||
exit_code = process.returncode
|
||||
if process.returncode != 0 and not quiet:
|
||||
|
|
|
|||
Loading…
Reference in a new issue