[FIX] script todo: dashboard Textual — lag et gel sur gros parc corrigés
Le TUI de suivi laggait à 30 VM et se figeait quand l'I/O ralentissait (disque plein / logs effacés). Cause : à chaque tick (1 s) on lisait le fichier log ENTIER de CHAQUE VM pour le statut + on relisait tout le log sélectionné, en synchrone sur la boucle d'événements Textual. Corrections : - read_status ne lit que les 4 derniers Ko (le marqueur de sortie est sur la dernière ligne) : un log de 2 Mo passe de « tout lire » à 0,1 ms. - _load_selected_log lit de façon INCRÉMENTALE (seek à l'offset) au lieu de relire tout le fichier. - Les VM TERMINÉES ne sont plus relues (cache _final). - _tick_table (2 s) et _tick_log (1 s) séparés et PROTÉGÉS par try/except : une erreur I/O transitoire (disque plein, log supprimé) ne tue plus la boucle -> l'interface reste réactive aux touches. - RichLog borné (max_lines=5000). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
51a9e7b97c
commit
b7522bb92d
1 changed files with 63 additions and 28 deletions
|
|
@ -148,14 +148,23 @@ def launch_installs(vms: list[dict], branch: str, remote_cmd: str) -> str:
|
|||
|
||||
|
||||
def read_status(log_path: str) -> tuple[str, int | None]:
|
||||
"""(état, code) d'un log : pending / running / done / failed."""
|
||||
"""(état, code) d'un log : pending / running / done / failed. Ne lit que
|
||||
la FIN du fichier (le marqueur de sortie est sur la dernière ligne) : lire
|
||||
tout le log de 30 VM chaque seconde saturait la boucle d'événements du TUI
|
||||
(lag, interface figée quand l'I/O ralentit)."""
|
||||
try:
|
||||
data = Path(log_path).read_text(errors="replace")
|
||||
size = os.path.getsize(log_path)
|
||||
if size == 0:
|
||||
return "pending", None
|
||||
with open(log_path, "rb") as fh:
|
||||
if size > 4096:
|
||||
fh.seek(-4096, os.SEEK_END)
|
||||
tail = fh.read().decode(errors="replace")
|
||||
except OSError:
|
||||
return "pending", None
|
||||
if not data.strip():
|
||||
if not tail.strip():
|
||||
return "pending", None
|
||||
for line in reversed(data.splitlines()):
|
||||
for line in reversed(tail.splitlines()):
|
||||
if EXIT_MARKER in line:
|
||||
try:
|
||||
code = int(line.split()[-1])
|
||||
|
|
@ -203,6 +212,9 @@ def run_monitor(manifest_path: str) -> None:
|
|||
self._offsets = {vm["name"]: 0 for vm in vms}
|
||||
self._selected = vms[0]["name"] if vms else None
|
||||
self._follow = True
|
||||
# Statuts TERMINAUX mémorisés : une VM finie n'est plus relue
|
||||
# (réduit fortement l'I/O sur un gros parc).
|
||||
self._final = {}
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
|
|
@ -213,7 +225,10 @@ def run_monitor(manifest_path: str) -> None:
|
|||
table.add_column("Durée", key="elapsed")
|
||||
for vm in vms:
|
||||
table.add_row(vm["name"], "⏳", "00:00", key=vm["name"])
|
||||
self._log = RichLog(id="log", highlight=False, markup=False)
|
||||
# max_lines borne la mémoire/rendu (un install verbeux × 30 VM).
|
||||
self._log = RichLog(
|
||||
id="log", highlight=False, markup=False, max_lines=5000
|
||||
)
|
||||
with Horizontal():
|
||||
yield table
|
||||
yield self._log
|
||||
|
|
@ -224,7 +239,10 @@ def run_monitor(manifest_path: str) -> None:
|
|||
self.title = "ERPLibre — suivi d'installation"
|
||||
self._refresh_ssh()
|
||||
self._load_selected_log(reset=True)
|
||||
self.set_interval(1.0, self._tick)
|
||||
# Table toutes les 2 s (30 lectures de fin de log), suivi du log
|
||||
# sélectionné toutes les 1 s (une seule lecture incrémentale).
|
||||
self.set_interval(2.0, self._tick_table)
|
||||
self.set_interval(1.0, self._tick_log)
|
||||
|
||||
# -- helpers -------------------------------------------------------- #
|
||||
def _vm_by_name(self, name):
|
||||
|
|
@ -244,39 +262,56 @@ def run_monitor(manifest_path: str) -> None:
|
|||
vm = self._vm_by_name(self._selected)
|
||||
if not vm:
|
||||
return
|
||||
name = vm["name"]
|
||||
if reset:
|
||||
self._log.clear()
|
||||
self._offsets[vm["name"]] = 0
|
||||
self._offsets[name] = 0
|
||||
# Lecture INCRÉMENTALE (seek à l'offset) : on ne relit pas tout le
|
||||
# fichier à chaque rafraîchissement.
|
||||
try:
|
||||
data = Path(vm["log"]).read_text(errors="replace")
|
||||
with open(vm["log"], "r", errors="replace") as fh:
|
||||
fh.seek(self._offsets.get(name, 0))
|
||||
new = fh.read()
|
||||
self._offsets[name] = fh.tell()
|
||||
except OSError:
|
||||
return
|
||||
off = self._offsets[vm["name"]]
|
||||
new = data[off:]
|
||||
if new:
|
||||
for line in new.splitlines():
|
||||
if EXIT_MARKER not in line:
|
||||
self._log.write(line)
|
||||
self._offsets[vm["name"]] = len(data)
|
||||
|
||||
def _tick(self):
|
||||
table = self.query_one("#vms", DataTable)
|
||||
for vm in vms:
|
||||
state, code = read_status(vm["log"])
|
||||
elapsed = time.time() - started
|
||||
mm, ss = divmod(int(elapsed), 60)
|
||||
label = ICON[state]
|
||||
if state == "failed":
|
||||
label = f"❌ ({code})"
|
||||
elif state == "done":
|
||||
label = "✅"
|
||||
table.update_cell(vm["name"], "state", label)
|
||||
if state in ("running", "pending"):
|
||||
table.update_cell(
|
||||
vm["name"], "elapsed", f"{mm:02d}:{ss:02d}"
|
||||
)
|
||||
def _tick_table(self):
|
||||
# Protégé : une erreur I/O transitoire (disque plein, log effacé)
|
||||
# ne doit pas tuer la boucle et figer l'interface.
|
||||
try:
|
||||
table = self.query_one("#vms", DataTable)
|
||||
for vm in vms:
|
||||
name = vm["name"]
|
||||
if name in self._final:
|
||||
continue # déjà terminé -> plus aucune lecture
|
||||
state, code = read_status(vm["log"])
|
||||
label = ICON[state]
|
||||
if state == "failed":
|
||||
label = f"❌ ({code})"
|
||||
elif state == "done":
|
||||
label = "✅"
|
||||
table.update_cell(name, "state", label)
|
||||
if state in ("running", "pending"):
|
||||
mm, ss = divmod(int(time.time() - started), 60)
|
||||
table.update_cell(
|
||||
name, "elapsed", f"{mm:02d}:{ss:02d}"
|
||||
)
|
||||
else:
|
||||
self._final[name] = (state, code)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _tick_log(self):
|
||||
if self._follow:
|
||||
self._load_selected_log()
|
||||
try:
|
||||
self._load_selected_log()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -- events --------------------------------------------------------- #
|
||||
def on_data_table_row_highlighted(self, event) -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue