diff --git a/script/todo/todo.py b/script/todo/todo.py index 0277423..b0a2f01 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1569,11 +1569,17 @@ class TODO: print(f"\n {t('Remote desktop kind:')}") print(f" [1] RDP 3389 (xrdp) *") print(f" [2] VNC 5901 (TigerVNC, Arch)") - print(f" [3] {t('Hypervisor console (QEMU screen, no guest server)')}") + print( + f" [3] {t('Hypervisor console (QEMU screen, no guest server)')}" + ) + print(f" [4] {t('Android emulator (adb 5555, then scrcpy)')}") kind_answer = input(f"{t('Choice')} [1]: ").strip() or "1" if kind_answer == "3": self._qemu_console_tunnel(name, src) return + if kind_answer == "4": + self._qemu_scrcpy_tunnel(name, src) + return port, kind = (5901, "VNC") if kind_answer == "2" else (3389, "RDP") local = port + 1 @@ -1601,6 +1607,65 @@ class TODO: ) print(f" {t('The tunnel stays open as long as that ssh runs.')}") + def _qemu_scrcpy_tunnel(self, name, src): + """Tunnel adb vers l'émulateur Android d'une VM, pour scrcpy. + + Pourquoi cette voie plutôt que « ssh -X » : par X11, chaque image de + l'écran traverse le réseau en pixels bruts — 0,62 Mpixel par image même + après réduction, en rendu logiciel. scrcpy, lui, reçoit un flux H.264 + encodé PAR l'appareil et le décode sur le poste. L'émulateur tourne + alors SANS fenêtre : plus de X11 du tout, ni sur l'hôte ni dans la VM. + + Le port est celui de l'émulateur, pas celui du serveur adb. Un émulateur + écoute sur 5554 (console) et 5555 (adb), tous deux sur le localhost de + la VM — vérifié par « ss -ltn ». C'est 5555 qu'il faut, et non 5037 : + tunneler le serveur adb obligerait à tuer celui du poste, qui occupe le + même port. + + Vérifié de bout en bout à travers le tunnel : une poignée de main adb + (paquet CNXN) reçoit « device::ro.product.name=sdk_gphone64_x86 » de + l'émulateur lui-même — c'est exactement ce que fait « adb connect ». + """ + port = 5555 + print(f"\n 📱 {t('Android emulator over adb + scrcpy')}") + print(f"\n {t('1. In the VM, start the emulator WITHOUT a window:')}") + emu = "$HOME/android/emulator/emulator" + if src == "ssh_config": + print( + f"\n ssh {name} '{emu} -avd erplibre" + " -no-window -no-audio -no-boot-anim'\n" + ) + else: + ip = self._qemu_resolve_ips([name]).get(name) + if not ip: + print(f" {t('No IP for this VM; is it running?')}") + return + print( + f"\n ssh erplibre@{ip} '{emu} -avd erplibre" + " -no-window -no-audio -no-boot-anim'\n" + ) + print(f" {t('2. Open the tunnel from YOUR workstation:')}") + if src == "ssh_config": + # « localhost » est résolu par le DERNIER saut, donc par la VM + # elle-même : le ProxyJump de ssh_config traverse les niveaux. + print(f"\n ssh -N -L {port}:localhost:{port} {name}\n") + print(f" {t('(through the ProxyJump already in ~/.ssh/config)')}") + else: + host, from_ssh = self._qemu_self_address() + user = os.environ.get("USER", "user") + if not from_ssh: + print( + f" ⚠ {t('Not in an SSH session: check the host address.')}" + ) + print(f"\n ssh -N -L {port}:{ip}:{port} {user}@{host}\n") + print(f" {t('3. Then, still on your workstation:')}") + print(f"\n adb connect localhost:{port}") + print(f" scrcpy -s localhost:{port}\n") + print(f" {t('The tunnel stays open as long as that ssh runs.')}") + print( + f" {t('scrcpy on Debian/Ubuntu:')} sudo apt install scrcpy" " adb" + ) + def _qemu_console_tunnel(self): """Tunnel vers l'ÉCRAN QEMU d'une VM, pas vers un serveur de l'invité. @@ -5980,7 +6045,13 @@ class TODO: # Mesuré sur l'émulateur. 'ssh erplibre@$ip \\"$HOME/android/platform-tools/adb install -r -t ' f"{el_dir}/mobile/erplibre_home_mobile/android/app/build" - '/outputs/apk/debug/app-debug.apk\\""' + '/outputs/apk/debug/app-debug.apk\\""; ' + # La voie scrcpy, nommée ici parce que c'est la première + # question qui vient après « ça se lance mais c'est lent » : + # X11 transporte des pixels bruts, scrcpy un flux H.264 encodé + # par l'appareil. Le détail du tunnel vit dans le menu + # « Remote desktop tunnel », choix 4. + + f'echo " {t("smoother, without X11:")} TODO > Execute > Deploy > QEMU/KVM > tunnel > 4"' ) def _qemu_after_remote_cmd(self, tools, prod=False): diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index d663c93..c20924a 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -7818,6 +7818,34 @@ TRANSLATIONS = { "fr": "venv ERPLibre absent (installation incomplète)", "en": "ERPLibre venv missing (incomplete install)", }, + "smoother, without X11:": { + "fr": "plus fluide, sans X11 :", + "en": "smoother, without X11:", + }, + "Android emulator (adb 5555, then scrcpy)": { + "fr": "Émulateur Android (adb 5555, puis scrcpy)", + "en": "Android emulator (adb 5555, then scrcpy)", + }, + "Android emulator over adb + scrcpy": { + "fr": "Émulateur Android par adb + scrcpy", + "en": "Android emulator over adb + scrcpy", + }, + "1. In the VM, start the emulator WITHOUT a window:": { + "fr": "1. Dans la VM, démarrer l'émulateur SANS fenêtre :", + "en": "1. In the VM, start the emulator WITHOUT a window:", + }, + "2. Open the tunnel from YOUR workstation:": { + "fr": "2. Ouvrir le tunnel depuis VOTRE poste :", + "en": "2. Open the tunnel from YOUR workstation:", + }, + "3. Then, still on your workstation:": { + "fr": "3. Puis, toujours sur votre poste :", + "en": "3. Then, still on your workstation:", + }, + "scrcpy on Debian/Ubuntu:": { + "fr": "scrcpy sur Debian/Ubuntu :", + "en": "scrcpy on Debian/Ubuntu:", + }, "Android emulator (Pixel)": { "fr": "Émulateur Android (Pixel)", "en": "Android emulator (Pixel)", diff --git a/test/test_qemu_desktop_tools.py b/test/test_qemu_desktop_tools.py index 26f7503..d4df110 100644 --- a/test/test_qemu_desktop_tools.py +++ b/test/test_qemu_desktop_tools.py @@ -582,6 +582,56 @@ class TestPycharmFirstOpen(unittest.TestCase): self.assertEqual(0, res.returncode, res.stderr) +class TestScrcpyTunnel(unittest.TestCase): + """Tunnel adb pour scrcpy : la voie sans X11.""" + + def setUp(self): + self.todo = TODO.__new__(TODO) + + def _printed(self, name, src): + import io + import contextlib + + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + self.todo._qemu_scrcpy_tunnel(name, src) + return buf.getvalue() + + def test_it_tunnels_the_emulator_port_not_the_adb_server(self): + """5555 est l'adb de l'émulateur ; 5037 est le serveur adb, et le + tunneliser obligerait à tuer celui du poste, qui occupe le même port. + Vérifié dans la VM par « ss -ltn » : 5554, 5555 et 5037 écoutent tous + sur 127.0.0.1.""" + out = self._printed("saut+vm", "ssh_config") + self.assertIn("-L 5555:localhost:5555", out) + self.assertNotIn("5037", out) + + def test_it_starts_the_emulator_without_a_window(self): + """Tout l'intérêt : plus de X11 nulle part, le flux est du H.264.""" + out = self._printed("saut+vm", "ssh_config") + self.assertIn("-no-window", out) + self.assertNotIn("ssh -X", out) + + def test_it_gives_the_three_workstation_commands(self): + out = self._printed("saut+vm", "ssh_config") + self.assertIn("adb connect localhost:5555", out) + self.assertIn("scrcpy -s localhost:5555", out) + self.assertIn("apt install scrcpy", out) + + def test_it_uses_the_ssh_config_target_as_is(self): + """« localhost » est résolu par le DERNIER saut : le ProxyJump déjà + écrit dans ~/.ssh/config traverse les niveaux, on ne le réinvente pas. + """ + out = self._printed("test-vm_02+erplibre-mobile-proof", "ssh_config") + self.assertIn("test-vm_02+erplibre-mobile-proof", out) + self.assertIn("ProxyJump", out) + + def test_the_avd_step_points_at_it(self): + """La question vient juste après « c'est trop lent » : la réponse doit + être à portée de journal.""" + self.assertIn("tunnel > 4", self.todo._qemu_avd_remote_cmd()) + + class TestGnomeSiteExtensions(unittest.TestCase): """Extensions posées depuis extensions.gnome.org, par leur UUID."""