feat: rendre les operations SFTP asynchrones
RemoteFilePanel appelait sftp.listdir_attr/mkdir/rename/remove/chmod/get directement depuis le thread GUI : un dossier distant lent ou une latence reseau gelait toute l'interface. Seuls upload/download passaient deja par un thread dedie (TransferWorker), et celui-ci reutilisait le meme SFTPClient que la navigation - paramiko.SFTPClient n'est pas concu pour etre pilote par plusieurs threads a la fois. - app/sftp/worker.py : SftpWorker, un thread proprietaire exclusif d'un canal SFTP, avec une file de jobs (callables) et les resultats renvoyes par signal Qt (marshalling automatique vers le thread GUI). - RemoteFilePanel soumet desormais toutes ses operations via ce worker avec callbacks succes/erreur ; un compteur de generation ignore les reponses de navigations devenues obsoletes (double-clic rapide, edition puis navigation ailleurs pendant le renvoi). - TransferWorker ouvre son propre canal SFTP (`sftp_factory`) au lieu de partager celui de la navigation : un gros transfert ne bloque plus le listage de dossiers. SFTPPanel(sftp, transfer_sftp_factory=...) permet d'injecter ce second canal (paramiko.SFTPClient.from_transport) ; a defaut, le canal de navigation est reutilise tel quel (cas des tests). - SessionTabWidget/SFTPTabWidget ouvrent ce second canal et appellent SFTPPanel.shutdown() a la fermeture de l'onglet (arrete proprement les threads avant destruction des widgets). tests/test_sftp.py adapte pour pomper la boucle d'evenements Qt apres les operations desormais asynchrones (meme couverture, timing different). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,19 @@ class FakeSFTP:
|
||||
|
||||
app = QApplication([])
|
||||
|
||||
|
||||
def wait_idle(panel_side, timeout=5.0):
|
||||
"""Pompe la boucle d'evenements Qt jusqu'a ce que toutes les operations SFTP
|
||||
soumises par ce panneau (navigate/refresh/etc, desormais asynchrones) soient
|
||||
revenues."""
|
||||
deadline = time.time() + timeout
|
||||
app.processEvents()
|
||||
while panel_side._callbacks and time.time() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.01)
|
||||
app.processEvents()
|
||||
|
||||
|
||||
remote_root = Path(tempfile.mkdtemp())
|
||||
local_root = Path(tempfile.mkdtemp())
|
||||
|
||||
@@ -85,6 +98,7 @@ local_root = Path(tempfile.mkdtemp())
|
||||
panel = SFTPPanel(FakeSFTP(remote_root))
|
||||
panel.local.navigate(str(local_root))
|
||||
panel.resize(1000, 600)
|
||||
wait_idle(panel.remote) # laisse le temps a la navigation initiale (asynchrone) d'arriver
|
||||
|
||||
print("=== 1. Listing distant avec colonnes ===")
|
||||
tree = panel.remote.tree
|
||||
@@ -135,11 +149,13 @@ assert got.exists()
|
||||
print()
|
||||
print("=== 5. Renommage distant ===")
|
||||
panel.remote.navigate("/")
|
||||
wait_idle(panel.remote)
|
||||
item = next(panel.remote.tree.topLevelItem(i) for i in range(panel.remote.tree.topLevelItemCount())
|
||||
if panel.remote.tree.topLevelItem(i).data(0, NAME_ROLE) == "config.yml")
|
||||
item.setSelected(True)
|
||||
panel.remote.sftp.rename("/config.yml", "/config_renomme.yml")
|
||||
panel.remote.refresh()
|
||||
wait_idle(panel.remote)
|
||||
names = [panel.remote.tree.topLevelItem(i).data(0, NAME_ROLE)
|
||||
for i in range(panel.remote.tree.topLevelItemCount())]
|
||||
print(" entrees apres renommage:", [n for n in names if n])
|
||||
@@ -155,6 +171,7 @@ print()
|
||||
print("=== 7. Suppression recursive distante ===")
|
||||
panel.remote._rmdir_recursive("/dossier_local")
|
||||
panel.remote.refresh()
|
||||
wait_idle(panel.remote)
|
||||
print(" dossier supprime du serveur:", not (remote_root / "dossier_local").exists())
|
||||
assert not (remote_root / "dossier_local").exists()
|
||||
|
||||
@@ -165,5 +182,7 @@ has_parent = any(panel.remote.tree.topLevelItem(i).data(0, PARENT_ROLE)
|
||||
print(" ligne '..' presente:", has_parent)
|
||||
assert has_parent
|
||||
|
||||
panel.shutdown()
|
||||
|
||||
print()
|
||||
print("TOUS LES TESTS SFTP OK")
|
||||
|
||||
Reference in New Issue
Block a user