From b6de9b5506a40242412d98cd47fa0dd8559c8f4e Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Tue, 11 Aug 2026 07:52:52 -0400 Subject: [PATCH] [ADD] tui qemu: resume an installation already running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs run detached (setsid -f): closing the terminal does not stop them, but it lost the only view onto them. With no way to resume, the only way out was deleting the VMs and starting over. Picking "Deploy" now looks at the latest run: if VMs there still lack an exit marker, it offers to reopen its monitoring instead of starting another. Time since the last write is shown, a dead run being otherwise indistinguishable from a live one. Only the latest run is examined: an old one left without a marker would flag a phantom install forever. Dry-run creates nothing, so it does not ask. "Reopen monitoring" moves to the Deployment section, where one looks for it. --- FR --- Les installs partent détachées (setsid -f) : fermer le terminal ne les arrête pas, mais faisait perdre la seule vue dessus. Sans moyen de reprendre, la seule issue était d'effacer les VM et de recommencer. Choisir « Déployer » regarde donc le dernier run : s'il lui reste des VM sans marqueur de sortie, il propose de rouvrir son suivi plutôt que d'en lancer un autre. Le silence depuis la dernière écriture est affiché, un run mort n'étant pas distinguable autrement d'un run vivant. Seul le dernier run est examiné : un run ancien laissé sans marqueur signalerait éternellement une install fantôme. L'aperçu ne crée rien, il ne pose pas la question. « Rouvrir le suivi » rejoint la section Déploiement, où on le cherche. Assisted-by: Claude Opus 5 --- script/todo/qemu_install_monitor.py | 40 +++++++++++++ script/todo/todo.py | 91 ++++++++++++++++++++++------- script/todo/todo_i18n.py | 24 ++++++++ 3 files changed, 134 insertions(+), 21 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 7b68617..d25ee20 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -261,6 +261,46 @@ def read_status(log_path: str) -> tuple[str, int | None]: return "running", None +def run_progress(run: dict) -> dict: + """Avancement d'un run : combien de VM tournent encore, et depuis quand + plus rien n'a été écrit. `idle` sert à distinguer une install vivante d'un + run laissé pour mort — le marqueur de sortie manque dans les deux cas.""" + active = final = 0 + latest = 0.0 + for vm in run.get("vms", []): + log = vm.get("log") or "" + state, _code = read_status(log) + if state in ("done", "failed"): + final += 1 + else: + active += 1 + try: + latest = max(latest, os.path.getmtime(log)) + except OSError: + pass + return { + "active": active, + "final": final, + "total": active + final, + "idle": (time.time() - latest) if latest else None, + } + + +def active_run(): + """Le run le PLUS RÉCENT s'il a encore des VM en cours, sinon None. + + Les installs tournent détachées (`setsid -f`) : fermer le terminal laisse + le travail se poursuivre mais fait perdre la seule vue dessus. On ne + regarde que le dernier run — un run ancien resté sans marqueur de sortie + signalerait éternellement une install fantôme.""" + runs = list_install_runs() + if not runs: + return None + run = dict(runs[0]) + run.update(run_progress(run)) + return run if run["total"] and run["active"] else None + + # Listes d'ignore reprises de script/test/run_parallel_test.py (erreurs/ # avertissements connus et bénins) : on réutilise la MÊME logique de détection # que la suite de tests ERPLibre pour analyser les logs d'installation. diff --git a/script/todo/todo.py b/script/todo/todo.py index f55798b..c4e7d7f 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1243,6 +1243,11 @@ class TODO: ) }, {"prompt_description": t("Download a cloud image only")}, + { + "prompt_description": t( + "Reopen install monitoring (last run / history)" + ) + }, {"section": t("Manage")}, {"prompt_description": t("List VMs (virsh list --all)")}, {"prompt_description": t("Show a VM IP address")}, @@ -1255,11 +1260,6 @@ class TODO: "Test a VM (open Odoo in a CLI browser)" ) }, - { - "prompt_description": t( - "Reopen install monitoring (last run / history)" - ) - }, {"prompt_description": t("Statistics (installs, durations, VMs)")}, { "prompt_description": t( @@ -1286,21 +1286,21 @@ class TODO: elif status == "3": self._qemu_download_image() elif status == "4": - self._qemu_list_vms(ask_advanced=True) - elif status == "5": - self._qemu_show_ip() - elif status == "6": - self._qemu_console() - elif status == "7": - self._qemu_resize_disk() - elif status == "8": - self._qemu_delete_vm() - elif status == "9": - self._qemu_cleanup() - elif status == "10": - self._qemu_test_vm() - elif status == "11": self._qemu_reopen_monitor() + elif status == "5": + self._qemu_list_vms(ask_advanced=True) + elif status == "6": + self._qemu_show_ip() + elif status == "7": + self._qemu_console() + elif status == "8": + self._qemu_resize_disk() + elif status == "9": + self._qemu_delete_vm() + elif status == "10": + self._qemu_cleanup() + elif status == "11": + self._qemu_test_vm() elif status == "12": self._qemu_stats() elif status == "13": @@ -2367,16 +2367,62 @@ class TODO: except ValueError: print(t("Invalid selection.")) return + self._qemu_open_monitor(run["manifest"]) + + def _qemu_open_monitor(self, manifest): + """Ouvre le dashboard sur un manifeste, en installant Textual au + besoin. Deux entrées y mènent — l'historique et la reprise proposée + avant un déploiement — d'où une seule définition.""" + from script.todo import qemu_install_monitor as mon + try: - mon.run_monitor(run["manifest"]) + mon.run_monitor(manifest) except ImportError: from script.todo import textual_setup if textual_setup.ensure(): - mon.run_monitor(run["manifest"]) + mon.run_monitor(manifest) except Exception as exc: print(f"{t('Command failed: ')}{exc}") + def _qemu_active_install(self): + """Propose de reprendre le suivi quand une installation tourne encore. + + Les installs partent détachées (`setsid -f`) : fermer le terminal ne + les arrête pas, mais faisait perdre la seule vue dessus, et la seule + issue connue était de tout effacer pour recommencer. + + True si l'on ne doit PAS enchaîner sur un déploiement.""" + try: + from script.todo import qemu_install_monitor as mon + + run = mon.active_run() + except Exception: + return False + if not run: + return False + names = ", ".join(v.get("name", "?") for v in run["vms"]) + print( + f"\n⏳ {t('An install is still running:')} {run['label']} — " + f"{run['active']}/{run['total']} {t('VM(s) in progress')}" + ) + print(f" {names}") + if run.get("idle") is not None: + # Un silence prolongé trahit un run mort dont le marqueur de sortie + # ne viendra jamais : l'utilisateur tranche mieux que nous. + print( + f" {t('Last activity:')} {mon._fmt_secs(int(run['idle']))}" + ) + print(f"\n [1] {t('Reopen that monitoring')} *") + print(f" [2] {t('Deploy anyway (new run)')}") + print(f" [0] {t('Back')}") + sel = input(t("Choice (number, blank = reopen): ")).strip() + if sel == "2": + return False + if sel != "0": + self._qemu_open_monitor(run["manifest"]) + return True + def _qemu_choose_cli_browser(self): """Offre la LISTE des navigateurs CLI installés, plus une option pour en INSTALLER un autre, et renvoie celui choisi, sinon None.""" @@ -5170,6 +5216,9 @@ class TODO: last = self._qemu_last_run_line() if last: print(last) + # Un aperçu ne crée rien : il n'a pas à interroger sur un run en cours. + if not dry_run and self._qemu_active_install(): + return self._qemu_check_libvirt_group() self._qemu_check_kvm() diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 41dd0ab..498841c 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1973,6 +1973,30 @@ TRANSLATIONS = { "fr": "RAM (Mo)", "en": "RAM (MB)", }, + "An install is still running:": { + "fr": "Une installation est encore en cours :", + "en": "An install is still running:", + }, + "VM(s) in progress": { + "fr": "VM en cours", + "en": "VM(s) in progress", + }, + "Last activity:": { + "fr": "Dernière activité il y a :", + "en": "Last activity:", + }, + "Reopen that monitoring": { + "fr": "Rouvrir ce suivi", + "en": "Reopen that monitoring", + }, + "Deploy anyway (new run)": { + "fr": "Déployer quand même (nouveau run)", + "en": "Deploy anyway (new run)", + }, + "Choice (number, blank = reopen): ": { + "fr": "Choix (numéro, vide = rouvrir) : ", + "en": "Choice (number, blank = reopen): ", + }, "free value…": { "fr": "valeur libre…", "en": "free value…",