[FIX] script todo: ne plus annoncer un émulateur qui n'a pas démarré
Trois défauts vus en conduisant le menu sur de vraies VM. « setsid » détache,
donc son code de retour vaut 0 même quand rien ne se lance : le menu disait
« Démarré » sur une VM sans SDK, dont le journal disait « not found ». Le
démarrage attend maintenant de VOIR le processus, et à défaut cite le journal.
Une sonde préalable lit binaire et AVD d'un coup : une VM déployée sans cocher
l'outil est le cas normal, pas une panne. Vérifié sur deux VM réelles — outillée
(True), migration (« aucun binaire emulator »), sans rien y démarrer.
Et tout ce qui n'était pas « 2 » démarrait l'émulateur : un « n » de travers
suffisait. Au passage, « Choice » n'était traduit dans aucun des trois menus
qui l'affichent.
--- EN ---
Three defects found while driving the menu against real VMs. setsid detaches,
so its exit code is 0 even when nothing launches: the menu said "Started" on a
VM with no SDK, whose log said "not found". Starting now waits to SEE the
process, and quotes the log when it never appears.
A prior probe reads binary and AVD in one go: a VM deployed without ticking the
tool is the normal case, not a failure. Verified on two real VMs — tooled
(True), migration ("no emulator binary") — without starting anything there.
And anything that was not "2" started the emulator; a stray "n" was enough.
Along the way, "Choice" was untranslated in all three menus showing it.
Assisted-by: Claude Opus 5
This commit is contained in:
parent
3b2f7fa3d6
commit
8f5a439148
3 changed files with 164 additions and 3 deletions
|
|
@ -1677,6 +1677,34 @@ class TODO:
|
|||
except (OSError, subprocess.SubprocessError, ValueError, IndexError):
|
||||
return -1
|
||||
|
||||
def _qemu_emulator_ready(self, target, src="virsh"):
|
||||
"""La VM a-t-elle de quoi émuler ? Rend (prêt, raison).
|
||||
|
||||
Le binaire ET l'AVD, en une seule lecture : sans cette vérification le
|
||||
démarrage détaché rendait 0 sur une VM sans SDK, et le menu annonçait
|
||||
« Démarré » quand le journal disait « not found ». Une VM déployée sans
|
||||
cocher l'outil Émulateur Android est le cas normal, pas une panne."""
|
||||
probe = (
|
||||
f"test -x {self._QEMU_EMULATOR_BIN} || echo NO_SDK; "
|
||||
f"test -d $HOME/.android/avd/{self._QEMU_AVD_NAME}.avd"
|
||||
" || echo NO_AVD"
|
||||
)
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["ssh"] + self._qemu_ssh_opts(src) + [target, probe],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=25,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False, t("Cannot reach this VM.")
|
||||
out = res.stdout or ""
|
||||
if "NO_SDK" in out:
|
||||
return False, t("No Android SDK in this VM: no emulator binary.")
|
||||
if "NO_AVD" in out:
|
||||
return False, t("No AVD named erplibre in this VM.")
|
||||
return True, ""
|
||||
|
||||
def _qemu_emulator_menu(self):
|
||||
"""Démarre l'émulateur Android d'une VM, et donne la suite qui va avec.
|
||||
|
||||
|
|
@ -1722,10 +1750,22 @@ class TODO:
|
|||
)
|
||||
print(f" {t('Closed.')}")
|
||||
|
||||
ready, why = self._qemu_emulator_ready(target, src)
|
||||
if not ready:
|
||||
print(f"\n ⚠ {why}")
|
||||
print(f" {t('Tick the Android emulator tool when deploying.')}")
|
||||
return
|
||||
|
||||
print(f"\n {t('Show a window?')}")
|
||||
print(f" [1] {t('No window - stream with scrcpy (smoother)')} *")
|
||||
print(f" [2] {t('Window over ssh -X (raw pixels, slower)')}")
|
||||
kind = input(f"{t('Choice')} [1]: ").strip() or "1"
|
||||
# Sans cette validation, TOUT ce qui n'est pas « 2 » démarrait
|
||||
# l'émulateur : une frappe de travers (« n ») lançait le démarrage,
|
||||
# observé. Un menu à deux crans n'a pas de troisième réponse.
|
||||
if kind not in ("1", "2"):
|
||||
print(t("Cancelled."))
|
||||
return
|
||||
emu = self._QEMU_EMULATOR_BIN
|
||||
avd = self._QEMU_AVD_NAME
|
||||
|
||||
|
|
@ -1760,6 +1800,27 @@ class TODO:
|
|||
if res.returncode:
|
||||
print(f" ⚠ {t('Could not start it:')} {res.stderr.strip()[:200]}")
|
||||
return
|
||||
# « setsid » détache : le code de retour ne dit RIEN de l'émulateur.
|
||||
# Le menu annonçait « Démarré » pendant que le journal de la VM disait
|
||||
# « not found » — mesuré sur une VM sans SDK. On attend donc de voir le
|
||||
# processus, et à défaut on rapporte le journal.
|
||||
for _ in range(5):
|
||||
if self._qemu_emulator_running(target, src) > 0:
|
||||
break
|
||||
time.sleep(2)
|
||||
else:
|
||||
print(f" ⚠ {t('It did not start; the VM log says:')}")
|
||||
log = subprocess.run(
|
||||
["ssh"]
|
||||
+ self._qemu_ssh_opts(src)
|
||||
+ [target, "tail -5 /tmp/erplibre-emulator.log 2>/dev/null"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=25,
|
||||
)
|
||||
for line in (log.stdout or "").strip().splitlines():
|
||||
print(f" {line}")
|
||||
return
|
||||
print(
|
||||
f" {t('Started. Boot takes about a minute; log in the VM:')}"
|
||||
" /tmp/erplibre-emulator.log"
|
||||
|
|
|
|||
|
|
@ -2252,6 +2252,27 @@ TRANSLATIONS = {
|
|||
"fr": "(l'hyperviseur ne fait que relayer ; -J met la VM en dernier)",
|
||||
"en": "(the hypervisor only relays; -J puts the VM last)",
|
||||
},
|
||||
"Choice": {"fr": "Choix", "en": "Choice"},
|
||||
"Tick the Android emulator tool when deploying.": {
|
||||
"fr": "Cochez l'outil Émulateur Android au déploiement.",
|
||||
"en": "Tick the Android emulator tool when deploying.",
|
||||
},
|
||||
"Cannot reach this VM.": {
|
||||
"fr": "VM injoignable.",
|
||||
"en": "Cannot reach this VM.",
|
||||
},
|
||||
"No Android SDK in this VM: no emulator binary.": {
|
||||
"fr": "Pas de SDK Android dans cette VM : aucun binaire emulator.",
|
||||
"en": "No Android SDK in this VM: no emulator binary.",
|
||||
},
|
||||
"No AVD named erplibre in this VM.": {
|
||||
"fr": "Aucun AVD nommé erplibre dans cette VM.",
|
||||
"en": "No AVD named erplibre in this VM.",
|
||||
},
|
||||
"It did not start; the VM log says:": {
|
||||
"fr": "Il n'a pas démarré ; le journal de la VM dit :",
|
||||
"en": "It did not start; the VM log says:",
|
||||
},
|
||||
"No ~/.ssh/config entry; see SSH configuration.": {
|
||||
"fr": "Aucune entrée ~/.ssh/config ; voir Configuration SSH.",
|
||||
"en": "No ~/.ssh/config entry; see SSH configuration.",
|
||||
|
|
|
|||
|
|
@ -109,6 +109,39 @@ class TestEmulatorRunning(unittest.TestCase):
|
|||
self.assertEqual(self.todo._qemu_emulator_running("h"), -1)
|
||||
|
||||
|
||||
class TestEmulatorReady(unittest.TestCase):
|
||||
"""Une seule lecture répond aux deux questions : binaire, puis AVD."""
|
||||
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
|
||||
def test_a_complete_vm_is_ready(self):
|
||||
with mock.patch("subprocess.run", return_value=_run_ok("")):
|
||||
self.assertEqual(self.todo._qemu_emulator_ready("h"), (True, ""))
|
||||
|
||||
def test_the_missing_piece_is_named(self):
|
||||
for probe, word in (("NO_SDK\n", "SDK"), ("NO_AVD\n", "AVD")):
|
||||
with mock.patch("subprocess.run", return_value=_run_ok(probe)):
|
||||
ready, why = self.todo._qemu_emulator_ready("h")
|
||||
self.assertFalse(ready, probe)
|
||||
self.assertIn(word, why, probe)
|
||||
|
||||
def test_the_sdk_is_reported_before_the_avd(self):
|
||||
"""Sans SDK, l'absence d'AVD n'est qu'une conséquence : nommer la cause
|
||||
évite d'envoyer l'utilisateur créer un AVD qu'il ne peut pas créer."""
|
||||
with mock.patch(
|
||||
"subprocess.run", return_value=_run_ok("NO_SDK\nNO_AVD\n")
|
||||
):
|
||||
_, why = self.todo._qemu_emulator_ready("h")
|
||||
self.assertIn("SDK", why)
|
||||
|
||||
def test_an_unreachable_vm_is_not_declared_ready(self):
|
||||
with mock.patch("subprocess.run", side_effect=OSError):
|
||||
ready, why = self.todo._qemu_emulator_ready("h")
|
||||
self.assertFalse(ready)
|
||||
self.assertTrue(why)
|
||||
|
||||
|
||||
class _MenuCase(unittest.TestCase):
|
||||
"""Socle commun : une VM locale, des réponses scriptées, aucun vrai ssh."""
|
||||
|
||||
|
|
@ -122,16 +155,29 @@ class _MenuCase(unittest.TestCase):
|
|||
self.todo._qemu_self_address = staticmethod(lambda: ("10.0.0.2", True))
|
||||
self.calls = []
|
||||
|
||||
def _play(self, answers, running=0, start_rc=0, port_taken=False):
|
||||
"""Joue le menu avec des réponses données ; rend (sortie, commandes)."""
|
||||
def _play(self, answers, running=0, start_rc=0, port_taken=False,
|
||||
probe="", running_after=1, log="rien"):
|
||||
"""Joue le menu avec des réponses données ; rend (sortie, commandes).
|
||||
|
||||
« running » est le compte AVANT le démarrage, « running_after » celui
|
||||
d'après : c'est cette distinction qui dit si l'émulateur a réellement
|
||||
pris, le code de retour d'un « setsid » détaché ne valant rien.
|
||||
"""
|
||||
state = {"started": False}
|
||||
|
||||
def fake_run(cmd, *a, **k):
|
||||
self.calls.append(cmd)
|
||||
joined = " ".join(cmd)
|
||||
if "pgrep -c qemu-system" in joined:
|
||||
return _run_ok(f"{running}\n")
|
||||
n = running_after if state["started"] else running
|
||||
return _run_ok(f"{n}\n")
|
||||
if "NO_SDK" in joined:
|
||||
return _run_ok(probe)
|
||||
if "setsid" in joined:
|
||||
state["started"] = True
|
||||
return _run_ok(returncode=start_rc, stderr="boum")
|
||||
if "tail -5" in joined:
|
||||
return _run_ok(log)
|
||||
return _run_ok()
|
||||
|
||||
it = iter(answers)
|
||||
|
|
@ -139,6 +185,8 @@ class _MenuCase(unittest.TestCase):
|
|||
"builtins.input", lambda *a: next(it)
|
||||
), mock.patch.object(
|
||||
TODO, "_port_in_use", staticmethod(lambda p: port_taken)
|
||||
), mock.patch(
|
||||
"script.todo.todo.time.sleep", lambda *a: None
|
||||
), mock.patch(
|
||||
"sys.stdout", new_callable=__import__("io").StringIO
|
||||
) as out:
|
||||
|
|
@ -221,6 +269,37 @@ class TestEmulatorMenu(_MenuCase):
|
|||
self.assertIn("boum", out)
|
||||
self.assertEqual(self._tunnels(calls), [])
|
||||
|
||||
def test_a_vm_without_the_sdk_is_diagnosed_before_anything_else(self):
|
||||
"""Une VM déployée sans cocher l'outil est le cas NORMAL. Le menu le
|
||||
dit avant même de demander la fenêtre — mesuré sur une VM de migration,
|
||||
où le démarrage détaché rendait 0 et le journal disait « not found »."""
|
||||
out, calls = self._play(["1"], probe="NO_SDK\n")
|
||||
self.assertIn("SDK", out)
|
||||
self.assertNotIn("[1]", out.split("VM locale")[-1])
|
||||
self.assertEqual(self._started(calls), [])
|
||||
|
||||
def test_a_vm_without_the_avd_is_named_as_such(self):
|
||||
out, calls = self._play(["1"], probe="NO_AVD\n")
|
||||
self.assertIn("AVD", out)
|
||||
self.assertEqual(self._started(calls), [])
|
||||
|
||||
def test_a_stray_answer_cancels_instead_of_starting(self):
|
||||
"""« n » à une question à deux crans partait démarrer l'émulateur :
|
||||
tout ce qui n'était pas « 2 » valait « sans fenêtre ». Observé."""
|
||||
for stray in ("n", "3", "oui"):
|
||||
self.calls = []
|
||||
out, calls = self._play(["1", stray])
|
||||
self.assertEqual(self._started(calls), [], stray)
|
||||
|
||||
def test_a_start_that_never_appears_reports_the_log_not_a_success(self):
|
||||
"""Le code de retour d'un « setsid » détaché vaut 0 quoi qu'il arrive :
|
||||
seule la présence du processus prouve le démarrage."""
|
||||
out, calls = self._play(
|
||||
["1", "1"], running_after=0, log="emulator: not found"
|
||||
)
|
||||
self.assertIn("not found", out)
|
||||
self.assertNotIn("scrcpy -s", out)
|
||||
|
||||
def test_a_successful_start_chains_into_the_tunnel_help(self):
|
||||
out, _ = self._play(["1", "1", "n"])
|
||||
self.assertIn("scrcpy", out)
|
||||
|
|
|
|||
Loading…
Reference in a new issue