From aa890c584711f3406d24eb004e072a3b255e9366 Mon Sep 17 00:00:00 2001 From: Florian Date: Sat, 5 Sep 2026 09:11:35 +0200 Subject: [PATCH] SFTP sur cette connexion : ne plus perdre la cible ssh detectee Signale par l'utilisateur avec une capture d'ecran : bouton "SFTP sur cette connexion" clique depuis un terminal local visiblement connecte en ssh (prompt root@debian:~#), formulaire de connexion vide a l'arrivee - "si je dois remplir le formulaire pour une session actuelle ca perd tout son sens". Cause reelle : detect_ssh_target() ne scrutait le texte du terminal qu'au moment du clic (buffer visible + historique pyte, 5000 lignes). Si la ligne `ssh user@hote` tapee par l'utilisateur n'y figure plus au moment du clic (beaucoup de sortie depuis, ecran efface...), plus rien n'est detectable - alors meme que la connexion ssh est toujours active. La capture montree correspond exactement a ce cas : un ecran efface apres la connexion. Corrige a la racine plutot qu'en agrandissant simplement l'historique scrute : app/ui/tabs.py (BaseTerminalTab) memorise desormais la cible des qu'une commande ssh apparait dans la sortie recue (_track_ssh_target, cablee comme terminal.feed sur data_received), et ne l'oublie plus jamais - detect_current_ssh_target() la renvoie en priorite, avec un dernier examen du buffer actuel seulement en repli. Le bouton "SFTP sur cette connexion" et le raccourci global Ctrl+Shift+F (ConnectionController._current_tab_ssh_target) utilisent tous les deux ce nouveau point d'entree unique. tests/test_ssh_target_tracking.py (4 verifications, nouveau fichier - ce bouton n'avait encore aucun test dedie) : memorisation a la frappe, cible toujours renvoyee meme quand le buffer live ne la contient plus, mise a jour vers une commande ssh plus recente, bouton emettant bien la cible memorisee. Suite complete revalidee (36/36), ruff propre. Co-Authored-By: Claude Sonnet 5 --- app/controllers/connection_controller.py | 3 +- app/ui/tabs.py | 22 +++++- tests/test_ssh_target_tracking.py | 95 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 tests/test_ssh_target_tracking.py diff --git a/app/controllers/connection_controller.py b/app/controllers/connection_controller.py index d672ef8..7afadd8 100644 --- a/app/controllers/connection_controller.py +++ b/app/controllers/connection_controller.py @@ -13,7 +13,6 @@ from app.dialogs.session_dialog import SessionDialog from app.key_manager import KeyManager from app.local_terminal import DEFAULT_SHELL, SHELL_LABELS from app.openssh.config import OpenSSHHost, session_from_openssh_host -from app.services.ssh_target import detect_ssh_target from app.session_manager import AUTH_PASSWORD, PASSWORD_STORAGE_SECURE, Session, SessionStore from app.ui.split_pane import LAYOUT_LABELS, PaneSlot, SplitContainer from app.ui.tabs import BaseTerminalTab, LocalTerminalTabWidget, SessionTabWidget, SFTPTabWidget @@ -181,7 +180,7 @@ class ConnectionController(QObject): "port": widget.session.port, } if isinstance(widget, BaseTerminalTab): - return detect_ssh_target(widget.terminal.buffer_text()) + return widget.detect_current_ssh_target() return None def _quick_connect(self, sftp: bool, target: dict | None = None) -> None: diff --git a/app/ui/tabs.py b/app/ui/tabs.py index 80448bb..563fe2e 100644 --- a/app/ui/tabs.py +++ b/app/ui/tabs.py @@ -151,6 +151,7 @@ class BaseTerminalTab(QWidget): self._log_file = None self._connection_generation = 0 self._closing = False + self._last_ssh_target: dict | None = None # ---------- journalisation ---------- def toggle_logging(self) -> None: @@ -193,6 +194,24 @@ class BaseTerminalTab(QWidget): except OSError: self.stop_logging() + # ---------- detection d'une commande ssh tapee a la main ---------- + def _track_ssh_target(self, _data: bytes) -> None: + """Reevalue a chaque sortie recue, et ne fait que memoriser un + resultat trouve (jamais l'oublier) : la commande `ssh ...` typee par + l'utilisateur peut ensuite defiler hors de l'ecran visible, voire hors + de l'historique conserve (beaucoup de sortie, `clear`...), longtemps + avant qu'on ait besoin de s'en souvenir (bouton SFTP sur cette + connexion).""" + target = detect_ssh_target(self.terminal.buffer_text()) + if target: + self._last_ssh_target = target + + def detect_current_ssh_target(self) -> dict | None: + """Cible ssh de ce terminal : la derniere commande ssh vue passer + (memorisee au moment ou elle etait encore visible), sinon un dernier + examen du texte actuellement disponible.""" + return self._last_ssh_target or detect_ssh_target(self.terminal.buffer_text()) + def _wire_connection(self) -> None: # Un signal deconnexion/erreur/connexion emis par une connexion remplacee # entre-temps (ex. reconnexion en cours) ne doit pas ecraser l'etat de la @@ -201,6 +220,7 @@ class BaseTerminalTab(QWidget): generation = self._connection_generation self.connection.connected.connect(lambda: self._on_current_generation(generation, self._on_connected)) self.connection.data_received.connect(self.terminal.feed) + self.connection.data_received.connect(self._track_ssh_target) self.connection.data_received.connect(self._log_output) self.connection.disconnected.connect( lambda reason: self._on_current_generation(generation, self._on_disconnected, reason) @@ -790,7 +810,7 @@ class LocalTerminalTabWidget(BaseTerminalTab): "Ouvre un gestionnaire de fichiers SFTP sur l'hote de la commande ssh tapee ici" ) self.sftp_btn.clicked.connect( - lambda: self.sftp_requested.emit(detect_ssh_target(self.terminal.buffer_text())) + lambda: self.sftp_requested.emit(self.detect_current_ssh_target()) ) self.bar.addWidget(self.sftp_btn) diff --git a/tests/test_ssh_target_tracking.py b/tests/test_ssh_target_tracking.py new file mode 100644 index 0000000..78d5f63 --- /dev/null +++ b/tests/test_ssh_target_tracking.py @@ -0,0 +1,95 @@ +"""Detection de la cible ssh "en cours" dans un terminal local : signale par +l'utilisateur (bouton "SFTP sur cette connexion" revenant a un formulaire +vide alors qu'une session ssh etait bien active a l'ecran). + +Cause reelle : detect_ssh_target() ne scrutait le texte du terminal qu'au +moment du clic, en repartant du buffer/historique ACTUEL - si la ligne +`ssh user@hote` tapee par l'utilisateur en etait sortie entre-temps (beaucoup +de sortie, ecran efface...), plus rien n'etait detectable, meme si la +connexion ssh etait toujours active. Corrige en memorisant la cible des +qu'elle apparait dans la sortie (BaseTerminalTab._track_ssh_target), pour ne +plus dependre de ce qui est encore present dans le buffer au moment du clic. + +Important : on alimente le terminal via connection.data_received.emit(...), +pas terminal.feed(...) directement - _track_ssh_target est cablee sur ce +signal (comme terminal.feed lui-meme), l'appeler directement contournerait le +suivi et ne testerait que le repli (deja fonctionnel avant ce correctif).""" +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) + +from unittest.mock import patch + +from PySide6.QtWidgets import QApplication + +from app.local_terminal import LocalTerminalConnection +from app.ui.tabs import LocalTerminalTabWidget + +app = QApplication.instance() or QApplication(sys.argv) + + +def make_tab(): + return LocalTerminalTabWidget() + + +def feed(tab, data: bytes) -> None: + tab.connection.data_received.emit(data) + + +print("=== 1. Une commande ssh tapee est memorisee des qu'elle apparait a l'ecran ===") +with patch.object(LocalTerminalConnection, "start", lambda self: None): + tab = make_tab() + assert tab.detect_current_ssh_target() is None + feed(tab, b"user@PC MINGW64 ~\r\n$ ssh root@debian\r\n") + target = tab.detect_current_ssh_target() + print(" cible detectee:", target) + assert target == {"user": "root", "host": "debian", "port": 22} + tab.close_connection() +print(" OK") + +print() +print("=== 2. La cible reste memorisee meme si la commande disparait du buffer ensuite ===") +with patch.object(LocalTerminalConnection, "start", lambda self: None): + tab = make_tab() + feed(tab, b"$ ssh root@debian\r\n") + assert tab._last_ssh_target == {"user": "root", "host": "debian", "port": 22} + + # simule la perte de la ligne d'origine (beaucoup de sortie, ecran efface...) : + # buffer_text() ne renvoie plus rien d'exploitable, mais la cible reste connue + with patch.object(type(tab.terminal), "buffer_text", return_value="root@debian:~# "): + target = tab.detect_current_ssh_target() + print(" cible apres 'perte' du buffer:", target) + assert target == {"user": "root", "host": "debian", "port": 22} + tab.close_connection() +print(" OK") + +print() +print("=== 3. Une nouvelle commande ssh remplace bien l'ancienne cible memorisee ===") +with patch.object(LocalTerminalConnection, "start", lambda self: None): + tab = make_tab() + feed(tab, b"$ ssh root@debian\r\n") + assert tab._last_ssh_target["host"] == "debian" + feed(tab, b"logout\r\n$ ssh admin@192.168.1.50\r\n") + target = tab.detect_current_ssh_target() + print(" nouvelle cible:", target) + assert target == {"user": "admin", "host": "192.168.1.50", "port": 22} + tab.close_connection() +print(" OK") + +print() +print("=== 4. Bouton 'SFTP sur cette connexion' : emet la cible memorisee, pas un formulaire vide ===") +with patch.object(LocalTerminalConnection, "start", lambda self: None): + tab = make_tab() + feed(tab, b"$ ssh root@debian\r\n") + with patch.object(type(tab.terminal), "buffer_text", return_value="root@debian:~# "): + received = [] + tab.sftp_requested.connect(lambda t: received.append(t)) + tab.sftp_btn.click() + print(" emis par le bouton:", received) + assert received == [{"user": "root", "host": "debian", "port": 22}] + tab.close_connection() +print(" OK") + +print() +print("SSH TARGET TRACKING : TOUS LES TESTS OK")