Fabric: Terminal local stdin détaché si l'exécution de ThreadingGroup inclut le sommeil

Créé le 25 juin 2018  ·  22Commentaires  ·  Source: fabric/fabric

J'utilise un groupe de threads pour exécuter des commandes shell. Après avoir exécuté un script qui inclut un sleep , le terminal local se retrouve avec stdin détaché (les frappes ne sont pas visibles sur la ligne de commande) et le terminal doit être réinitialisé.

J'ai essayé cela plusieurs fois et je trouve que cela ne se produit qu'avec ThreadingGroups (les SerialGroups sont corrects). La commande sleep peut se trouver n'importe où dans une seule ligne (première commande, milieu, dernière) et peut être jointe dans une seule ligne avec des points-virgules ou une double esperluette. Toutes les commandes s'exécutent comme prévu, mais le terminal reste dans un mauvais état.

Curieusement, si l'exécution précédente s'est terminée avec une exception non interceptée, le terminal ne sera pas affecté.

Reproduire:

from fabric import ThreadingGroup as Group

# raise ValueError()
remotes = Group("host1.example.com", "host2.example.com")
result = remotes.run("echo 1; sleep 1; echo 2")

Exécutez le script ci-dessus. Une fois terminé, tapez quelque chose sur la ligne de commande. Si vous ne voyez aucune sortie, <ctrl>+c et tapez reset<enter> . Pour voir le comportement post-exception, décommentez la ligne raise , exécutez le code, commentez la ligne et exécutez deux fois de plus. La première exécution réussie laissera le terminal en bon état. Le second laissera stdin détaché.

J'ai découvert ce problème avec sleep lors de mes tests, mais il est possible que d'autres commandes aient le même effet. Il y a aussi une chance que je fasse quelque chose de mal. Si c'est le cas, toutes mes excuses.

Ma configuration :
python 3.6.4
tissu 2.1.3
OSX 10.13.5, connexion à Ubuntu 14.04

Bug Needs investigation

Tous les 22 commentaires

Voir #1814 comme deuxième cas de problème reproductible possible.

Cela ressemble à un bug légitime pour moi et je ne suis pas sûr de ce qui le cause à première vue. Ça sent comme ça pourrait être un problème Unix générique avec des tuyaux terminaux attachés à plusieurs sous-processus à la fois, ou (surtout en passant par l'exemple de #1814) une condition de concurrence autour de l'état du tuyau, ou quelque chose comme ça.

J'essaierai de reproduire et d'embrouiller une cause/solution.

En outre, cela nécessite probablement une correction au niveau Invoke et peut être purement dans son domaine (dans la mesure où je n'ai tout simplement pas encore fait grand-chose avec le threading dans un contexte Invoke pur ; mais voir par exemple pyinvoke/invoke#194 - c'est une chose qui devrait arriver là aussi). Dans ce cas, je déplacerai cela vers un ticket là-bas et le "correctif" Fabric serait de mettre à niveau son Invoke une fois le correctif publié.

J'étais sur Ubuntu 16.04.2 et je me connectais au même.

Un autre rapport du même problème dans #1829. C'est dans mon prochain jalon de correction de bogues et je me concentrerai sur ce prochain jour OSS, espérons-le, (lundi).

J'ai juste essayé de reproduire cela (branche 2.0, Python 3.6.4, macOS 10.12) et je n'y suis malheureusement pas parvenu. J'ai d'abord essayé le double localhost, puis deux instances de cloud distantes distinctes, sans aucun dés de toute façon ; mon terminal va bien après.

Je vais essayer un peu un conteneur Linux au cas où cela aiderait, mais comme l'OP était également sur macOS, je ne suis pas sûr que cela fasse une différence. Je vais également essayer de l'exécuter en boucle pour voir s'il ne s'agit que d'une reproduction occasionnelle.

Je vais également l'essayer sur 2.1 au cas où nous l'aurions introduit en 2.1, bien que cela semble très improbable.

@jensenak @nicktimko reproduisez-vous cela 100 % du temps ? 50 % ? 5 % ?

@bitprophet sur 2.1.3, cela se produisait assez souvent dans mon flux de travail réel (> 80%, j'allais également en parallèle sur 6 serveurs, pas 2), bien que dans mon exemple artificiel de #1814, c'est beaucoup plus bas, peut-être 20%. Je peux essayer de créer une configuration Docker ou, à défaut, une configuration Vagrant à reproduire.

@bitprophet Cela a été 100% du temps pour moi. Juste pour être sûr, j'ai commencé un nouveau virtualenv avec seulement Fabric installé. J'ai testé 2.0, 2.1 et 2.2. L'exemple de code que j'ai collé a produit le comportement décrit à chaque fois. Dans tous les tests, je me connectais à des télécommandes Ubuntu 14.04.

Je suis sur une version différente d'OSX (10.13). C'est peut-être lié ? Bien que @nicktimko n'était pas du tout sur OSX.

Au cas où une autre version poserait problème, voici à quoi ressemblait pip freeze dans mon virtualenv :

asn1crypto==0.24.0
bcrypt==3.1.4
cffi==1.11.5
cryptography==2.3
fabric==2.2.1
idna==2.7
invoke==1.1.0
paramiko==2.4.1
pyasn1==0.4.4
pycparser==2.18
PyNaCl==1.2.1
six==1.11.0

Étant donné que tous ces éléments ont été installés en tant que dépendances de Fabric 2.2, je m'attendrais à ce que vos versions se ressemblent.

S'il y a plus que je peux faire pour aider, je suis plus que disposé. Je ne sais pas vraiment où chercher.

Avec quel commit dois-je tester ; avez-vous fait des changements récemment qui pourraient affecter les choses? Je vais essayer avec le gel ci-dessus, vous pouvez également fournir un autre reqs.txt gelé et je peux voir si cela fonctionne/ne fonctionne pas pour moi.

@nicktimko @jensenak Merci pour l'info supplémentaire. Je vais continuer à essayer de le reproduire ici ; à 20%, je ne l'aurais certainement pas assez essayé pour déclencher. Mes télécommandes ont été Mac et certaines Debian plus anciennes, je peux essayer Ubuntu Trusty au cas où cela serait spécifique à cela (ce qui serait étrange, mais bon, tout cela est étrange.)

De plus, quels sont vos environnements shell locaux ? Le mien est zsh sur (encore une fois, macOS 10.12) intégré Terminal.app, à l'intérieur de tmux. Je vais aussi essayer quelques permutations autour de cet angle dans un instant.

AHA. Cela semble être spécifique à bash ! Était toujours incapable de reproduire sous zsh en dehors de tmux, mais au moment où j'essaie sous bash, j'obtiens immédiatement les symptômes mentionnés. Idem à l'intérieur de tmux, donc tmux n'a aucune incidence - c'est un truc de shell.

_Pourquoi_ cela se comporterait différemment sous bash vs zsh, je n'en ai aucune idée dans l'immédiat. Cela pourrait-il être spécifique à la façon dont ils sont implémentés, ou (cela semble plus probable) peut-être que quelque chose dans mes fichiers dotfiles zsh empêche le problème? Devra creuser... bien que l'identification d'une solution du côté Python soit nécessaire dans les deux cas très probablement.

EDIT: également, la reproduction se produit même lorsque vous vous connectez plusieurs fois au sshd de mon hôte local, ce qui n'est pas trop surprenant. L'extrémité distante semble donc sans importance.

De plus, j'ai essayé de vérifier la note sur "l'exécution précédente, à l'exception de l'empêchement du problème pour l'exécution suivante uniquement", mais cela ne s'est pas produit pour moi; Je reçois le comportement à chaque fois, peu importe.

Moar : J'ai supprimé le sleep pour voir ce qui se passerait ; Je suis toujours capable de reproduire, même si c'est maintenant légèrement plus intermittent (bien que ce n'est pas quelque chose de facile à reproduire dans une boucle automatique, c'est une reproduction à la main, ce qui signifie un faible nombre de cas de test, ce qui signifie que le vrai % d'occurrence sera réel difficile à mesurer avec précision.)

C'est aussi bien, moins il y a de déclencheurs bizarres, mieux c'est. Cela sent comme _devrait_ être une erreur de thread basique et stupide quelque part, qui ne serait généralement pas affectée par quoi que ce soit de spécifique sur l'extrémité distante ou locale à part la durée qui rend une condition de concurrence (ou w/e) plus probable.

Vous vous demandez si cela est lié à pyinvoke/invoke#552 qui se résume à la sous-classe de thread de gestion des exceptions d'Invoke (utilisée dans ThreadingGroup ici) ayant peut-être foiré la détection de la mort du thread.

Je vais devoir m'assurer que je comprends cela (son correctif potentiel, pyinvoke/invoke#553, n'était pas une fusion insta car il semblait étrange que nous ayons obtenu quelque chose d'apparemment fonctionnel, donc faux) et ensuite voir si l'appliquer fait ce symptôme disparaît.

J'ai enlevé le sommeil pour voir ce qui se passerait ; Je suis toujours capable de reproduire, même si c'est maintenant un peu plus intermittent

Cela ressemble au cas de test que j'avais, où j'avais besoin de le frapper plusieurs fois avant qu'il ne tombe en panne. On dirait que tu maîtrises bien ça

J'ai remarqué aujourd'hui que je ne pouvais pas non plus reproduire le comportement d'exception que j'avais décrit il y a un mois... malheureusement je ne me souviens plus de ce que je faisais alors. :/

Je cours en effet bash ici. Bonne trouvaille. Le fait que le problème soit intermittent sans sommeil me fait me demander s'il s'agit d'une condition de course quelconque.

Vous dites ça, mais maintenant je ne peux plus le reproduire, ou du moins c'est TRÈS intermittent. Remettre le sommeil le fait revenir beaucoup plus souvent. Je dois aimer les conditions de course.

En regardant ce problème d'Invoke, le journaliste mentionne même un terminal défectueux comme symptôme ; mais étrangement je ne peux pas reproduire ce symptôme même sous bash, avec son code. Je ne serais toujours pas surpris si la cause première est la même (cela a à voir avec quelques problèmes autour de la mort du thread et de la fermeture de stdin, ou peut-être du retour à la mise en mémoire tampon par ligne, correctement avant de quitter).

Vérification des taches de l'autre problème mentionné, contre le cas repro ici :

  • le bit ExceptionHandlingThread.is_dead ne semble pas avoir d'importance, il est vraisemblablement correct, ce qui a du sens car il est destiné à gérer les exceptions dans le thread et aucun de ces cas ne gère réellement les exceptions. is_dead est False pour les 3 threads de travail (stdin/out/err) alors que je m'y attendrais.
  • l'affirmation selon laquelle nous ne fermons pas correctement le sous-processus stdin semble plus proche de la marque ; si cela laisse le stdin du terminal de contrôle attaché à un descripteur de fichier maintenant mort ou quelque chose ...? (Je devrais vraiment mieux savoir ce qui se passe dans ce cas de toute façon.)

    • Sauf que... dans le cas de Fabric, il n'y a pas de sous-processus local et pas de transmission directe des descripteurs de fichiers, donc cela ne peut pas être le cas.

    • Cela signifie-t-il que le problème est plus susceptible d'être autre?


Essayer une autre tactique... qu'en est-il exactement de l'environnement du terminal après l'apparition du bogue, a-t-il changé ? En exécutant stty -a sous bash avec et sans corruption de bogue, les différences que je peux voir sont :

  • lflags : le terminal buggé a -icanon , -echo , -pendin (vs un terme ordinaire où ceux-ci n'ont pas tous un signe moins). Ne pas faire écho semble certainement être un problème, en supposant que c'est ce que cela signifie.
  • iflags : buggé a -ixany et ignpar (le premier exemple de quelque chose étant défini, pas désinstallé, dans la mauvaise configuration)
  • oflags et cflags identiques, tout comme cchars (je serais vraiment bizarre si les caractères de contrôle avaient changé...)

Selon man stty :

  • icanon contrôle le traitement ERASE et KILL ; probablement pas une grosse différence (mais pourquoi cela est défini ou non défini peut être intéressant)
  • echo est ce à quoi cela ressemble, s'il faut faire écho, et c'est clairement le plus gros problème pratique du bogue.
  • pendin indique si l'entrée (en supposant que stdin) est en attente après un basculement canonique (et puisque icanon est clairement inversé... oui) et sera ré-entrée lorsqu'une lecture devient en attente ou plus d'entrée arrive. On ne sait pas pourquoi cela est important, ou pourquoi il est défini normalement et désactivé lorsqu'il est buggé (je m'attendais à ce dernier, le cas échéant.)
  • ixany permet à n'importe quel caractère de « démarrer la sortie » (et lorsqu'il n'est pas défini, n'autorise que START. ok ?)
  • ignpar signifie ignorer (ou désactiver, pour ne pas ignorer) les caractères avec des erreurs de parité.

Dans l'ensemble, on a l'impression qu'un "mode" de niveau supérieur est appliqué au terminal, de la même manière que nous avons défini stdin sur une lecture avec tampon de caractères pour nous permettre de lire 1 octet à la fois au lieu d'attendre que l'utilisateur entre en purée.

Ce qui ressemble au comportement affiché (en quelque sorte...), et sur lequel je me posais la question plus tôt ; mais en lisant le code en question (parce que le patch Invoke le mentionne également, bien que re: thread death), le changement de mode est formulé comme un gestionnaire de contexte, il devrait donc toujours être désactivé, quelle que soit la manière dont nous sortons de cette boucle. Mais il va falloir que je vérifie ça maintenant.

Mineur : il suffit de dire stty echo pour définir echo pour « réparer » un terminal ; même si icanon , pendin etc ne sont toujours pas définis. Ça n'aide pas vraiment mais bon, c'est bon à savoir je suppose.

D'ACCORD! Je pense l'avoir compris en regardant ce gestionnaire de contexte : c'est probablement parce que le gestionnaire de contexte prend des instantanés de l'état du terminal actuel pour la restauration à la fin du bloc. Mais que fait-on dans ce cas ? Nous exécutons _deux threads de haut niveau distincts_, chacun exécutant sa _propre copie_ de ce gestionnaire de contexte !

Et dans Invoke, bien que nous ayons l'intention d'être thread-safe, nous ne testons actuellement rien d'autre que nos propres threads d'E/S de bas niveau ; 99% de la "sécurité des threads" est simplement l'utilisation de l'état d'objet autonome au lieu de l'état de module global horrible de Fabric 1. Ainsi, cette partie particulière de la gestion de l'état n'est jamais exécutée simultanément avec elle-même (en partie parce que "l'état" est littéralement le terminal de contrôle, dont il n'y en a qu'un, donc... l'état global...).

Je ne l'ai pas encore prouvé à 100% (sur le point de le faire) mais il n'y a aucun moyen que ce ne soit pas le cas. Le thread qui s'exécute en second est très susceptible de prendre un instantané des attributs du terminal de contrôle _après_ le premier thread l'a déjà mis en mode tampon de caractères ; puis, si ce deuxième thread _finit_ également (encore une fois, probablement mais pas certain), il "restaure" le mauvais état, annulant efficacement la restauration du premier thread.

Confirmé que l'indicateur ECHO, par exemple, est définitivement pris en compte par le gestionnaire de contexte qui n'est pas le premier, puis restauré par celui-ci. Travailler sur une solution, qui, je pense, finira par être "essayez de déterminer si setcbreak semble déjà appliqué, et no-op dans ce cas au lieu de faire la danse snapshot-modify-restore".

Devrait avoir l'effet escompté, est légèrement plus propre au démarrage (n'exécute jamais setcbreak > 1 fois) et évite un cas critique où un correctif naïf pourrait toujours simplement définir ECHO, etc. tty-like mais était _déjà_ configuré pour ne pas être en écho. (Peu probable, bien sûr, mais probablement pas impossible.)

Puisqu'il s'agit d'un problème Invoke uniquement, je vais lui donner une maison sur ce tracker - je m'attends à ce qu'un test et une correction soient bientôt effectués, mais si vous avez tous autre chose à ajouter, veuillez passer à https ://github.com/pyinvoke/invoke/issues/559

Pour être clair, une fois que cela est corrigé, cela devrait être disponible dans Invoke 1.0.2/1.1.1 (et peut-être 1.2.0 si je le sors en même temps) et _no_ Les mises à niveau de Fabric devraient être nécessaires, uniquement Invoke.

@bitprophet Super ! Cela fonctionne après la mise à niveau d'Invoke :)
Je vous remercie pour vos efforts.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

26huitailang picture 26huitailang  ·  3Commentaires

bitprophet picture bitprophet  ·  6Commentaires

bitprophet picture bitprophet  ·  4Commentaires

jmcgrath207 picture jmcgrath207  ·  5Commentaires

SamuelMarks picture SamuelMarks  ·  3Commentaires