[ADD] script todo: dire depuis quand un journal d'installation est muet

Le sablier ne distingue pas une installation qui travaille d'une qui est morte :
le marqueur de sortie manque dans les deux cas. Une session ssh emportée, et le
tableau de bord a affiché « ⏳ » pendant 54 minutes sans que rien ne cloche à
l'œil.

La colonne d'état porte maintenant le silence du journal — « ⏳ silence 48min ».
C'est un chiffre, pas un verdict. Le seuil de dix minutes vient d'une mesure :
le téléchargement d'Android Studio tient ~5 min sans une ligne, et l'étape
« APK debug » davantage, son détail partant dans le journal de la VM. Plus bas,
chaque installation deviendrait une alerte, et l'alerte cesserait d'être lue.

--- EN ---

The hourglass does not tell a working install from a dead one: the exit marker
is missing in both cases. An ssh session was reaped, and the dashboard showed
"⏳" for 54 minutes with nothing looking wrong.

The state column now carries the log's silence — "⏳ silent 48min". It is a
figure, not a verdict. The ten-minute threshold comes from a measurement: the
Android Studio download holds ~5 min without a line, and the "debug APK" step
longer, its detail going to the VM's own log. Any lower and every install would
become an alert, and the alert would stop being read.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 07:26:54 -04:00
parent 087ce3effb
commit b4310665fc
3 changed files with 97 additions and 1 deletions

View file

@ -332,6 +332,41 @@ def read_status(log_path: str) -> tuple[str, int | None]:
return "running", None
# Au-delà de ce silence, la colonne d'état le DIT. Ce n'est pas un verdict mais
# un chiffre : plusieurs étapes sont légitimement muettes, leur sortie partant
# ailleurs. Mesuré sur une installation réelle : le téléchargement d'Android
# Studio tient ~5 min sans une ligne, et l'étape « APK debug » davantage — son
# détail va dans le journal de la VM. Dix minutes passent donc au-dessus du
# premier sans attendre le second, qui reste bruyant par nature.
#
# À 48 minutes, le chiffre est accablant : une installation est morte ainsi,
# session ssh emportée, et le sablier tournait toujours.
IDLE_HINT_SECS = 600
def log_idle(log_path: str) -> float:
"""Secondes depuis la dernière écriture dans le journal. -1 s'il manque.
La date de modification du fichier, et non un compte de lignes : c'est la
seule mesure qui distingue « rien n'avance » de « rien ne s'affiche »."""
try:
return max(0.0, time.time() - os.path.getmtime(log_path))
except OSError:
return -1.0
def state_mark(icon: str, idle: float) -> str:
"""Icône d'état, suivie du silence du journal quand il dépasse le seuil.
Le silence est une INFORMATION, pas un diagnostic : plusieurs étapes sont
muettes longtemps sans rien avoir de cassé. Mais le sablier seul ne
distingue pas une installation qui travaille d'une qui est morte, et c'est
arrivé — 48 minutes de sablier sur une session ssh déjà emportée."""
if idle > IDLE_HINT_SECS:
return f"{icon} {t('silent')} {_fmt_secs(idle)}"
return icon
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
@ -1682,7 +1717,12 @@ def run_monitor(manifest_path: str, run_app: bool = True):
table, name, "state", f"⏸ {t('paused')}"
)
else:
self._set_cell(table, name, "state", ICON[state])
self._set_cell(
table,
name,
"state",
state_mark(ICON[state], log_idle(vm["log"])),
)
ref = eta_reference(self._stats, vm.get("arch"))
if ref is not None:
remaining.append(max(0, ref - (now - started)))

View file

@ -2259,6 +2259,7 @@ TRANSLATIONS = {
"Grouped by shape": {"fr": "Regroupé par forme", "en": "Grouped by shape"},
"line": {"fr": "ligne", "en": "line"},
"free space": {"fr": "libre", "en": "free"},
"silent": {"fr": "silence", "en": "silent"},
"inotify watches raised for the IDE": {
"fr": "watches inotify relevées pour l'IDE",
"en": "inotify watches raised for the IDE",

View file

@ -93,6 +93,61 @@ class TestMemSegment(unittest.TestCase):
self.assertIn("0K/12.0G", m._mem_tele(12 * GB, 13 * GB, 0, 0))
class TestLogSilence(unittest.TestCase):
"""Une installation morte et une qui travaille portent le même sablier.
Vécu : une session ssh emportée, l'installation morte sans marqueur de
sortie, et le tableau de bord a montré « ⏳ » pendant 54 minutes. Le
marqueur manque dans les deux cas — seule la date d'écriture du journal
les sépare.
"""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.log = Path(self.tmp.name) / "vm.log"
self.log.write_text("== installation ==\n")
def tearDown(self):
self.tmp.cleanup()
def test_a_fresh_log_is_not_silent(self):
self.assertLess(m.log_idle(str(self.log)), 5)
def test_an_old_log_reports_its_age(self):
import os
import time
old = time.time() - 3000
os.utime(self.log, (old, old))
self.assertAlmostEqual(m.log_idle(str(self.log)), 3000, delta=5)
def test_a_missing_log_is_not_reported_as_silent(self):
"""-1 plutôt que 0 : « absent » n'est pas « à l'instant », et surtout
pas « silencieux depuis toujours »."""
self.assertEqual(m.log_idle("/nonexistent/erplibre.log"), -1.0)
self.assertEqual(m.state_mark("⏳", -1.0), "⏳")
def test_below_the_threshold_the_cell_stays_bare(self):
"""Plusieurs étapes sont muettes quelques minutes — un téléchargement
d'Android Studio, une compilation dont la sortie va ailleurs."""
self.assertEqual(m.state_mark("⏳", 0), "⏳")
self.assertEqual(m.state_mark("⏳", m.IDLE_HINT_SECS), "⏳")
def test_above_the_threshold_the_cell_says_how_long(self):
mark = m.state_mark("⏳", 2900)
self.assertNotEqual(mark, "⏳")
self.assertIn("48", mark)
def test_the_threshold_clears_the_longest_measured_silence(self):
"""Mesuré : le téléchargement d'Android Studio reste ~5 min sans une
ligne. Un seuil en dessous transformerait chaque installation en alerte,
et l'alerte cesserait d'être lue."""
studio_download = 5 * 60
self.assertGreater(m.IDLE_HINT_SECS, studio_download)
# Et pas si haut qu'une installation morte passe la demi-heure.
self.assertLessEqual(m.IDLE_HINT_SECS, 1200)
class TestTheRealBar(unittest.TestCase):
"""La barre telle que le suivi la construit, sans lancer la TUI."""