Fabric: O stdin do terminal local é detectado se a execução do ThreadingGroup inclui suspensão

Criado em 25 jun. 2018  ·  22Comentários  ·  Fonte: fabric/fabric

Estou usando um grupo de threading para executar comandos shell. Depois de executar um script que inclui sleep , o terminal local é deixado com stdin desanexado (pressionamentos de tecla não visíveis na linha de comando) e o terminal deve ser reiniciado.

Eu tentei isso algumas vezes e descobri que isso só acontece com ThreadingGroups (SerialGroups está bem). O comando sleep pode estar em qualquer lugar em uma linha (primeiro comando, meio, último) e pode ser unido em uma linha com ponto-e-vírgulas ou "e comercial" duplo. Todos os comandos são executados conforme o esperado, mas o terminal permanece em mau estado.

Estranhamente, se a execução anterior foi encerrada com uma exceção não capturada, o terminal não será afetado.

Reproduzir:

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")

Execute o script acima. Depois de sair, digite algo na linha de comando. Se você não vir nenhuma saída, <ctrl>+c e digite reset<enter> . Para ver o comportamento pós-exceção, descomente a linha raise , execute o código, comente a linha e execute mais duas vezes. A primeira execução bem-sucedida deixará o terminal em bom estado. O segundo deixará stdin destacado.

Descobri esse problema com sleep em meus testes, mas é possível que outros comandos tenham o mesmo efeito. Também existe a chance de eu estar apenas fazendo algo errado. Se for esse o caso, minhas desculpas.

Minha configuração:
python 3.6.4
tecido 2.1.3
OSX 10.13.5, conectando-se ao Ubuntu 14.04

Bug Needs investigation

Todos 22 comentários

Veja # 1814 como possível segunda pasta de problemas reproduzível.

Isso soa como um bug legítimo para mim e não tenho certeza do que está causando isso imediatamente. Parece que pode ser um problema Unix genérico com tubos de terminal sendo anexados a vários subprocessos ao mesmo tempo, ou (esp indo pelo exemplo do # 1814) uma condição de corrida em torno do estado do tubo, ou algo parecido.

Tentará reproduzir e confundir uma causa / solução.

Além disso, isso provavelmente requer correção no nível de Invoke e pode ser puramente em seu domínio (na medida em que eu simplesmente não fiz muito com encadeamento em um contexto de Invoke puro ainda; mas veja, por exemplo, pyinvoke / invoke # 194 - é uma coisa que deve acontecer lá também). Nesse caso, vou mover isso para um tíquete lá e a "correção" do Fabric seria atualizar o Invoke assim que a correção for lançada.

Eu estava no Ubuntu 16.04.2 conectando ao mesmo.

Outro relatório do mesmo problema em # 1829. Este é o meu próximo marco de correção de bug e estarei focando no próximo dia de OSS (segunda-feira).

Eu apenas tentei reproduzir isso (branch 2.0, Python 3.6.4, macOS 10.12) e não consegui, infelizmente. Primeiro tentei o host local duplo, depois duas instâncias de nuvem remota separadas, sem dados de qualquer maneira; meu terminal está bem depois.

Tentarei um contêiner Linux daqui a pouco, apenas no caso de ajudar, mas como o OP também estava no macOS, não tenho certeza se fará diferença. Também tentarei executá-lo em um loop para ver se é apenas uma reprodução ocasional.

Também irei experimentá-lo no 2.1 caso o tenhamos introduzido de alguma forma no 2.1, embora isso pareça muito improvável.

@jensenak @nicktimko você está reproduzindo isso 100% do tempo? 50%? 5%?

@bitprophet no 2.1.3 estava acontecendo em meu fluxo de trabalho real com bastante frequência (> 80%, eu também estava indo em paralelo a 6 servidores, não 2), embora em meu exemplo inventado de # 1814 seja muito menor, talvez 20%. Posso tentar criar uma configuração do Docker ou, em caso de falha, uma configuração do Vagrant para reproduzir.

@bitprophet Isso tem sido 100% do tempo para mim. Só para ter certeza, comecei um novo virtualenv com apenas o tecido instalado. Testei 2.0, 2.1 e 2.2. O código de exemplo que colei produziu o comportamento descrito todas as vezes. Em todos os testes, conectei-me a controles remotos do Ubuntu 14.04.

Estou em uma versão diferente do OSX (10.13). Talvez isso esteja relacionado? Embora @nicktimko não estivesse no OSX.

No caso de outra versão ser um problema, eis a aparência de pip freeze no meu 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

Visto que todos eles foram instalados como dependências do fabric 2.2, espero que suas versões sejam semelhantes.

Se houver mais que eu possa fazer para ajudar, estou mais do que disposto. Só não tenho certeza de onde mais procurar.

Com qual commit devo testar; você fez alguma alteração recentemente que pode afetar as coisas? Vou tentar com o congelamento acima, você também pode fornecer outro reqs.txt congelado e posso ver se funciona / não funciona para mim.

@nicktimko @jensenak Obrigado pela informação extra. Vou continuar tentando reproduzi-lo aqui; a 20%, eu definitivamente não teria tentado o suficiente para desencadear. Meus controles remotos têm sido Mac e alguns Debians mais antigos, posso tentar o Ubuntu Trusty caso seja de alguma forma específico para isso (o que seria estranho, mas ei, essa coisa toda é estranha).

Além disso, quais são os seus ambientes de shell locais? O meu é zsh no (novamente, macOS 10.12) Terminal.app embutido, dentro do tmux. Também tentarei algumas permutações em torno desse ângulo daqui a pouco.

AHA. Isso parece ser específico do bash! Ainda não consegui reproduzir no zsh fora do tmux, mas no momento em que tento no bash, recebo imediatamente os sintomas mencionados. Idem dentro do tmux, então o tmux não tem suporte - é uma coisa de shell.

_Por que_ isso se comportaria de maneira diferente em bash vs zsh, não tenho ideia imediata. Pode ser específico sobre como eles são implementados ou (parece mais provável) talvez algo em meus dotfiles zsh esteja evitando o problema? Terá que cavar ... embora identificar uma solução no lado do Python seja necessária de qualquer maneira, muito provavelmente.

EDITAR: também, a reprodução acontece mesmo quando conectado ao sshd do meu host local várias vezes ao mesmo tempo, o que não é muito surpreendente. Portanto, o fim remoto parece não importar.

Além disso, tentei verificar a observação sobre "a exceção da execução anterior evita o problema apenas na próxima execução", mas isso não ocorreu para mim; Eu consigo o comportamento sempre, independentemente.

Moar: Eu removi sleep para ver o que aconteceria; Ainda sou capaz de reproduzir, embora agora esteja um pouco mais intermitente (embora não seja algo fácil de reproduzir em um loop automático, é tudo reprodução manual, o que significa um baixo número de casos de teste, o que significa que a% verdadeira de ocorrência será real difícil de medir com precisão.)

Isso também é bom, quanto menos gatilhos estranhos, melhor. Isso tem o cheiro de _deve_ ser algum erro de threading básico e idiota em algum lugar, que normalmente não seria afetado por nada específico na extremidade remota ou local, exceto pelo tempo que torna uma condição de corrida (ou w / e) mais provável.

Querendo saber se isso está relacionado a pyinvoke / invoke # 552 que se resume à subclasse de thread de manipulação de exceção do Invoke (usada em ThreadingGroup aqui), possivelmente tendo deturpado a detecção de morte de thread.

Preciso ter certeza de que entendi isso (sua correção potencial, pyinvoke / invoke # 553, não foi uma fusão instantânea, pois parecia estranho que tivéssemos obtido algo aparentemente funcional, tão errado) e ver se aplicá-lo faz este sintoma desaparece.

Tirei o sono para ver o que aconteceria; Ainda sou capaz de reproduzir, embora agora seja um pouco mais intermitente

Parece o caso de teste que tive, no qual precisei acertar algumas vezes antes de estourar. Parece que você tem um bom controle sobre isso

Percebi hoje que também não consegui reproduzir o comportamento de Exceção que descrevi há um mês ... infelizmente não me lembro o que estava fazendo naquela época. : /

Estou realmente executando o bash aqui. Bom achado. O fato de o problema ser intermitente sem o sono me faz pensar se isso é algum tipo de condição de corrida.

Você diz isso, mas agora não posso reproduzi-lo novamente, ou pelo menos é MUITO intermitente. Colocar o sono de volta faz com que ele apareça com muito mais frequência. Tenho que amar as condições de corrida.

Olhando para o problema do Invoke, o repórter até menciona um terminal bagunçado como um sintoma; mas estranhamente eu não consigo reproduzir _that_ sintoma mesmo sob o bash, com seu código. Ainda não ficaria surpreso se a causa raiz fosse a mesma (tem a ver com algumas coisas em torno da morte do thread e stdin sendo fechado, ou talvez definido de volta para buffering linewise, antes de sair).

Verificando as manchas o outro problema mencionado, contra o caso repro aqui:

  • o bit ExceptionHandlingThread.is_dead não parece importar, ele está presumivelmente correto, o que faz algum sentido, visto que se destina a lidar com exceções no thread e nenhum desses casos realmente trata de exceções. is_dead é False para todos os 3 threads de trabalho (stdin / out / err) quando eu esperava que fosse.
  • a afirmação de que não estamos fechando corretamente o subprocesso 'stdin parece estar mais perto do alvo; se isso deixa o stdin do terminal de controle anexado a um descritor de arquivo morto ou algo assim ...? (Eu realmente deveria saber melhor o que acontece neste caso de qualquer maneira.)

    • Exceto ... no caso do Fabric, não há subprocesso local e nem passagem direta de descritores de arquivo, então esse não pode ser o caso.

    • O que significa que é mais provável que o problema seja outra coisa?


Tentando outra abordagem ... o que exatamente mudou sobre o ambiente do terminal depois que o bug apareceu? Executando stty -a no bash com e sem a presença de corrupção de bug, as diferenças que posso ver são:

  • lflags : o terminal bugado tem -icanon , -echo , -pendin (vs o termo regular onde todos não têm um sinal de menos). Não ecoar certamente parece um problema, presumindo que seja isso o que isso significa.
  • iflags : bugged-out tem -ixany e ignpar (o primeiro exemplo de algo sendo configurado, não desarmado, na configuração incorreta)
  • oflags e cflags idênticos, assim como cchars (eu ficaria muito estranho se os caracteres de controle tivessem mudado ...)

De acordo com man stty :

  • icanon controla o processamento de ERASE e KILL; provavelmente não é uma grande diferença (embora seja interessante definir por que está ativado ou desativado)
  • echo é o que parece, se deve ecoar, e é claramente o maior problema prático do bug.
  • pendin indica se a entrada (presumindo stdin) está pendente após uma troca canônica (e já que icanon está claramente invertido ... sim) e será reintroduzida quando uma leitura se tornar pendente ou mais entradas chega. Não está claro por que isso é importante ou por que é definido normalmente e não definido quando bugado (eu teria esperado o último, se houver.)
  • ixany permite que qualquer caractere 'inicie a saída' (e quando não definido, permite apenas INICIAR. Ok?)
  • ignpar significa ignorar (ou não definir, para não ignorar) caracteres com erros de paridade.

Em suma, parece que algum 'modo' de nível superior está sendo aplicado ao terminal, semelhante a como definimos stdin para leitura em buffer de caracteres para nos permitir ler 1 byte por vez, em vez de esperar até que o usuário entre com o mashes.

Que soa como o comportamento em exibição (mais ou menos ...), e sobre o qual eu estava pensando antes; mas lendo o código em questão (porque aquele patch do Invoke o menciona também, embora re: thread death), a mudança de modo é expressa como um gerenciador de contexto, portanto, _deve_ sempre ficar desarmado, independentemente de como saímos desse loop. Mas vou precisar verificar isso três vezes agora.

Menor: simplesmente dizer stty echo para definir echo é suficiente para 'consertar' um terminal; mesmo se icanon , pendin etc ainda não estiverem definidos. Realmente não ajuda, mas hey, bom saber, eu acho.

OK! Acho que descobri, enquanto olhava para aquele contextmanager: provavelmente é porque o contextmanager captura o estado atual do terminal para restauração no fechamento do bloco. Mas o que estamos fazendo neste caso? Estamos executando _duas threads de alto nível_ separadas, cada uma executando sua _própria cópia_ deste gerenciador de contexto!

E no Invoke, embora pretendamos ser thread-safe, não testamos nada além de nossos próprios threads de IO de baixo nível; 99% da "segurança de thread" é ​​simplesmente o uso do estado de objeto autocontido em vez do estado de módulo global horribad do Fabric 1. Portanto, esse bit particular de manutenção de estado nunca é executado simultaneamente com ele mesmo (em parte porque o "estado" é literalmente o terminal de controle, do qual existe apenas um, então ... estado global ...).

Eu não provei 100% ainda (estou prestes a), mas não tem como não ser isso. O encadeamento que é executado em segundo lugar tem grande probabilidade de fazer um instantâneo dos atributos do terminal de controle _após_ o primeiro encadeamento já configurá-lo no modo de buffer de caracteres; então, se aquele segundo encadeamento também _concluir_ em segundo (novamente, provavelmente, mas não certo), ele "restaura" o estado incorreto, desfazendo efetivamente a restauração do primeiro encadeamento.

Confirmado que o sinalizador ECHO, por exemplo, está definitivamente sendo capturado pelo gerenciador de contexto que não é o primeiro e, em seguida, restaurado pelo mesmo. Trabalhar em uma solução, que eu acho que vai acabar sendo apenas "tentar descobrir se setcbreak parece já aplicado, e no-op nesse caso, em vez de fazer a dança instantâneo-modificar-restaurar".

Deve ter o efeito pretendido, é marginalmente mais limpo para inicializar (nunca executa setcbreak> 1 vez) e evita um caso de canto em que uma correção ingênua pode sempre apenas definir ECHO, etc como "ligado" - o que seria interrompido em situações onde o fluxo em questão é semelhante a tty, mas foi _already_ definido para não ter eco. (Improvável, claro, mas provavelmente não impossível.)

Como esse é um problema apenas da Invoke, vou dar uma olhada nesse rastreador - estou esperando fazer um teste e uma correção para isso em breve, mas se vocês tiverem mais alguma coisa a adicionar, vá para https : //github.com/pyinvoke/invoke/issues/559

Para ficar bem claro, uma vez que isso seja corrigido, deve ser lançado em Invoke 1.0.2 / 1.1.1 (e possivelmente 1.2.0 se eu conseguir isso ao mesmo tempo) e _nenhuma_ atualização do Fabric deve ser necessária, apenas Invoke.

@bitprophet Ótimo! Funciona após a atualização do Invoke :)
Obrigado pelo seu esforço.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

SamuelMarks picture SamuelMarks  ·  3Comentários

neemxyang picture neemxyang  ·  6Comentários

omzev picture omzev  ·  6Comentários

peteruhnak picture peteruhnak  ·  4Comentários

supriyopaul picture supriyopaul  ·  4Comentários