Na verdade, ele estava em um subcomponente de visualizador de imagens de terceiros do nosso aplicativo.
Descobrimos que 2-3 dos usuários de nosso aplicativo frequentemente faziam com que o componente visualizador de imagens lançasse uma exceção e morresse horrivelmente. No entanto, tivemos dezenas de outros usuários que nunca viram o problema, apesar de usar o aplicativo para a mesma tarefa na maior parte do dia útil. Também havia um usuário em particular que o recebia com muito mais frequência do que o resto deles.
Tentamos as etapas usuais:
(1) Eles trocaram de computador com outro usuário que nunca teve o problema de descartar o computador / configuração. - O problema os seguiu.
(2) Eles fizeram o login no aplicativo e funcionaram como um usuário que nunca viu o problema. - O problema ainda os seguia.
(3) O usuário relatou qual imagem estava visualizando e configurou um equipamento de teste para repetir a visualização da imagem milhares de vezes em rápida sucessão. O problema não se apresentou no arnês.
(4) Mandou um desenvolvedor sentar com os usuários e assisti-los o dia todo. Eles viram os erros, mas não os notaram fazendo nada fora do comum para causá-los.
Lutamos com isso por semanas tentando descobrir o que os "Usuários de erro" tinham em comum que os outros usuários não tinham. Não tenho idéia de como, mas o desenvolvedor na etapa (4) teve um momento eureka no caminho para trabalhar um dia digno da Enciclopédia Brown.
Ele percebeu que todos os "Usuários de erro" eram canhotos e confirmou esse fato. Somente usuários canhotos têm os erros, nunca Righties. Mas como ser canhoto pode causar um bug?
Nós o fizemos sentar e assistir os canhotos novamente prestando atenção especificamente a qualquer coisa que eles estivessem fazendo de maneira diferente, e foi assim que descobrimos.
Aconteceu que o bug só acontecia se você movesse o mouse para a coluna de pixels mais à direita no visualizador de imagens enquanto carregava uma nova imagem (erro de estouro porque o fornecedor tinha um cálculo único para o evento de passagem do mouse).
Aparentemente, enquanto esperavam a próxima imagem carregar, todos os usuários naturalmente moveram a mão (e, portanto, o mouse) em direção ao teclado.
O único usuário que cometeu o erro com mais frequência foi um desses tipos de ADD que movia o mouse compulsivamente com muita impaciência enquanto esperava a próxima página carregar, portanto, movia o mouse para a direita muito mais rapidamente e pressionava o botão no momento certo para que ela fizesse isso quando o evento de carregamento aconteceu. Até recebermos uma correção do fornecedor, pedimos para ela soltar o mouse depois de clicar (próximo documento) e não tocá-lo até carregar.
Daí em diante, era conhecido na lenda da equipe de desenvolvedores como "The Left Handed Bug"
Isso é de muito tempo atrás (o final dos anos 80).
A empresa em que trabalhei escreveu um pacote CAD (no FORTRAN) que rodava em várias estações de trabalho Unix (HP, Sun, Silcon Graphics etc.). Usamos nosso próprio formato de arquivo para armazenar os dados e, quando o pacote foi iniciado, o espaço em disco era escasso; portanto, havia muita troca de bits usada para armazenar vários sinalizadores nos cabeçalhos das entidades.
O tipo da entidade (linha, arco, texto etc) foi multiplicado por 4096 (eu acho) quando armazenado. Além disso, esse valor foi negado para indicar um item excluído. Então, para obter o tipo, tínhamos um código que:
Em todas as máquinas, exceto uma, isso indicava ± 1 (para uma linha), ± 2 (para um arco) etc. e então poderíamos verificar o sinal para ver se foi excluído.
Em uma máquina (eu acho que a HP), tivemos um problema estranho, onde o manuseio de itens excluídos estava estragado.
Isso foi nos dias que antecederam o IDE e os depuradores visuais, então eu tive que inserir instruções de rastreamento e log para tentar rastrear o problema.
Acabei descobrindo que era porque, enquanto todos os outros fabricantes implementavam
MOD
, o que-4096 MOD 4096
resultou na-1
HP o implementou matematicamente corretamente, o que-4096 MOD 4096
resultou-4097
.Acabei tendo que passar por toda a base de código, salvando o sinal do valor e tornando-o positivo antes de executar o
MOD
e multiplicando o resultado pelo valor do sinal.Isso levou vários dias.
fonte
Uau, boa leitura aqui!
O mais difícil foi anos atrás, quando o Turbo Pascal era grande, embora pudesse ter sido um dos primeiros IDEs C ++ da época. Como desenvolvedor único (e terceiro participante desta startup), escrevi algo como um programa CAD simplificado e amigável para o vendedor. Foi ótimo na época, mas desenvolveu um acidente aleatório desagradável. Era impossível reproduzir, mas acontecia com bastante frequência que eu partia em uma caçada a insetos.
Minha melhor estratégia era dar um passo no depurador. O bug acontecia apenas quando o usuário havia inserido um desenho suficiente e talvez tivesse que estar em um determinado modo ou estado de zoom, portanto havia muitas configurações tediosas e pontos de interrupção de limpeza, funcionando normalmente por um minuto para inserir um desenho e, em seguida, percorra um grande pedaço de código. Especialmente úteis foram os pontos de interrupção que pulariam um número ajustável de vezes e depois quebrariam. Todo esse exercício teve que ser repetido várias vezes.
Eventualmente, reduzi-o a um local onde uma sub-rotina estava sendo chamada, recebendo um 2, mas de dentro dela vi algum número sem sentido. Eu poderia ter percebido isso antes, mas não havia entrado nessa sub-rotina, supondo que ela recebesse o que lhe foi dado. Cego ao assumir que as coisas mais simples estavam bem!
Acabou colocando um int de 16 bits na pilha, mas a sub-rotina esperava 32 bits. Ou algo assim. O compilador não preencheu automaticamente todo o valor para 32 bits ou fez uma verificação de tipo suficiente. Era trivial consertar, apenas parte de uma linha, quase nenhum pensamento necessário. Mas para chegar lá foram necessários três dias de caça e questionamento do óbvio.
Portanto, tenho experiência pessoal com essa história sobre o consultor caro, depois de um tempo dá um toque em algum lugar e cobra US $ 2.000. Os executivos exigem um colapso, e são US $ 1 para a torneira, US $ 1999 para saber onde tocar. Exceto no meu caso, era hora e não dinheiro.
Lições aprendidas: 1) use os melhores compiladores, onde "melhor" é definido como incluindo a verificação de tantos problemas quanto a ciência da computação sabe verificar e 2) questiona as coisas óbvias simples ou, pelo menos, verifica seu funcionamento adequado.
Desde então, todos os bugs difíceis têm sido realmente difíceis, pois sei verificar as coisas simples com mais detalhes do que parece necessário.
A lição 2 também se aplica ao bug eletrônico mais difícil que já corrigi, também com uma correção trivial, mas vários EEs inteligentes foram interrompidos por meses. Mas este não é um fórum de eletrônicos, então não vou dizer mais nada.
fonte
A condição de corrida de dados de rede do inferno
Eu estava escrevendo um cliente / servidor de rede (Windows XP / C #) para trabalhar com um aplicativo semelhante em uma estação de trabalho muito antiga (Encore 32/77) criada por outro desenvolvedor.
O que o aplicativo fez essencialmente foi compartilhar / manipular certos dados no host para controlar o processo do host que está executando o sistema com nossa sofisticada interface do usuário com tela sensível ao toque para vários monitores baseada em PC.
Isso foi feito com uma estrutura de três camadas. O processo de comunicação leu / gravou dados para / do host, realizou todas as conversões de formato necessárias (endianness, formato de ponto flutuante etc.) e gravou / leu os valores para / de um banco de dados. O banco de dados atuou como um intermediário de dados entre as comunicações e as UIs da tela sensível ao toque. O aplicativo da interface do usuário da tela de toque gerou interfaces de tela de toque com base em quantos monitores foram conectados ao PC (ele detectou isso automaticamente).
No período de tempo determinado, um pacote de valores entre o host e o nosso PC só podia enviar 128 valores no máximo por fio, com uma latência máxima de ~ 110ms por ida e volta (o UDP era usado com uma conexão Ethernet direta de x-over entre os computadores). Portanto, o número de variáveis permitidas com base no número variável de telas sensíveis ao toque anexadas estava sob controle estrito. Além disso, o host (apesar de ter uma arquitetura multiprocessador bastante complexa com barramento de memória compartilhada usado para computação em tempo real) tinha cerca de 1/100 da capacidade de processamento do meu telefone celular, por isso foi encarregado de fazer o menor processamento possível e seu servidor / client teve que ser escrito em assembly para garantir isso (o host estava executando uma simulação em tempo real que não podia ser afetada pelo nosso programa).
A questão era. Alguns valores, quando alterados na tela sensível ao toque, não pegam apenas o valor recém-inserido, mas alternam aleatoriamente entre esse valor e o valor anterior. Isso e apenas em alguns valores específicos em algumas páginas específicas com uma certa combinação de páginas já exibiu o sintoma. Quase perdemos o problema completamente até começarmos a executá-lo no processo inicial de aceitação do cliente
Para definir o problema, escolhi um dos valores oscilantes:
Então eu comecei o wireshark e comecei a decodificar manualmente as capturas de pacotes. Resultado:
Percorri todos os detalhes do código de comunicação cem vezes, sem encontrar falhas / erros.
Finalmente, comecei a enviar e-mails para o outro desenvolvedor, perguntando em detalhes como o fim dele funcionava para ver se havia algo que estava faltando. Então eu encontrei.
Aparentemente, quando ele enviou dados, ele não liberou a matriz de dados antes da transmissão; portanto, basicamente, ele estava substituindo o último buffer usado com os novos valores substituindo os antigos, mas os valores antigos não substituídos ainda estão sendo transmitidos.
Portanto, se um valor estivesse na posição 80 da matriz de dados e a lista de valores solicitados mudasse para menos de 80, mas esse mesmo valor estivesse contido na nova lista, os dois valores existiriam no buffer de dados para esse buffer específico em qualquer Tempo dado.
O valor lido do banco de dados dependia do intervalo de tempo em que a interface do usuário estava solicitando o valor.
A correção foi dolorosamente simples. Leia o número de itens recebidos no buffer de dados (na verdade, ele estava contido como parte do protocolo de pacote) e não leia o buffer além desse número de itens.
Lições aprendidas:
Não tome como garantido o poder da computação moderna. Houve um tempo em que os computadores não eram compatíveis com Ethernet e a descarga de uma matriz podia ser considerada cara. Se você realmente deseja ver até onde chegamos, imagine um sistema que praticamente não tem forma de alocação dinâmica de memória. Ou seja, o processo executivo teve que pré-alocar toda a memória para todos os programas em ordem e nenhum programa poderia crescer além desse limite. IE, alocar mais memória para um programa sem recompilar todo o sistema pode causar uma falha maciça. Eu me pergunto se as pessoas vão falar sobre os dias pré-coleta de lixo à mesma luz algum dia.
Ao trabalhar em rede com protocolos personalizados (ou manipular a representação de dados binários em geral), certifique-se de ler as especificações até entender todas as funções de todos os valores enviados pelo canal. Quero dizer, leia até seus olhos doerem. As pessoas manipulam dados manipulando bits ou bytes individuais, têm maneiras muito inteligentes e eficientes de fazer as coisas. A falta dos mínimos detalhes pode danificar o sistema.
O tempo total para consertar foi de 2 a 3 dias, com a maior parte do tempo gasto trabalhando em outras coisas quando fiquei frustrado com isso.
Nota: o computador host em questão não suporta ethernet por padrão. O cartão para conduzi-lo foi feito sob medida e adaptado e a pilha de protocolos praticamente não existia. O desenvolvedor com quem eu estava trabalhando era um grande programador, ele não apenas implementou uma versão simplificada do UDP e uma pilha Ethernet falsa mínima (o processador não era poderoso o suficiente para lidar com uma pilha Ethernet completa) no sistema para este projeto mas ele fez isso em menos de uma semana. Ele também fora um dos líderes originais da equipe do projeto que havia projetado e programado o SO em primeiro lugar. Vamos apenas dizer, qualquer coisa que ele já tenha compartilhado sobre computadores / programação / arquitetura, não importa quanto tempo acabe ou quanto eu já seja novo, eu ouviria cada palavra.
fonte
O fundo
O inseto
Como eu o encontrei
No começo, eu tinha certeza de que era um problema de desempenho normal, então criei um log elaborado. O desempenho verificado em todas as chamadas conversava com o pessoal do banco de dados sobre a utilização, observava os servidores em busca de problemas. 1 semana
Então eu tinha certeza de que tinha um problema de contenção de threads. Eu verifiquei que meus deadlocks tentavam criar a situação, criamos ferramentas para tentar criar a situação na depuração. Com a crescente frustração da gerência, voltei-me aos meus colegas sobre como sugerir coisas de reiniciar o projeto do zero para limitar o servidor a um encadeamento. 1,5 semanas
Então, olhei para o blog de Tess Ferrandez, criei um arquivo de despejo de usuário e o anexei com windebug na próxima vez que o servidor fez um despejo. Descobri que todos os meus threads estavam presos na função dictionary.add.
O longo e pequeno dicionário pequeno que apenas controlava qual log gravar erros de x threads não foi sincronizado.
fonte
Tínhamos um aplicativo que estava conversando com um dispositivo de hardware que, em alguns casos, falharia em funcionar corretamente se o dispositivo fosse desconectado fisicamente até que ele fosse reconectado e redefinido duas vezes.
O problema acabou sendo que um aplicativo em execução na inicialização ocasionalmente falhava quando tentava ler de um sistema de arquivos que ainda não havia sido montado (por exemplo, se um usuário o configurou para ler em um volume NFS). Na inicialização, o aplicativo envia alguns ioctls ao driver para inicializar o dispositivo, depois lê as definições de configuração e envia mais ioctls para colocar o dispositivo no estado correto.
Um erro no driver fazia com que um valor inválido fosse gravado no dispositivo quando a chamada de inicialização foi feita, mas o valor foi substituído por dados válidos depois que as chamadas foram feitas para colocar o dispositivo em um estado específico.
O dispositivo em si tinha uma bateria e detectaria se perdesse energia da placa-mãe e gravaria um sinalizador na memória volátil, indicando que havia perdido energia, entraria em um estado específico na próxima vez em que fosse ligado e em um dispositivo específico. É necessário enviar instruções para limpar a bandeira.
O problema era que, se a energia fosse removida depois que os ioctls fossem enviados para inicializar o dispositivo (e gravassem o valor inválido no dispositivo), mas antes que dados válidos pudessem ser enviados. Quando o dispositivo era ligado novamente, veria que o sinalizador havia sido definido e tentava ler os dados inválidos enviados pelo driver devido à initalização incompleta. Isso colocaria o dispositivo em um estado inválido, onde o sinalizador de desligamento havia sido apagado, mas o dispositivo não receberia mais instruções até que fosse reinicializado pelo driver. A segunda redefinição significaria que o dispositivo não estava tentando ler os dados inválidos armazenados nele e receberia instruções de configuração corretas, permitindo que elas fossem colocadas no estado correto (supondo que o aplicativo que envia os ioctls não tenha falhado) )
No final, demorou cerca de duas semanas para descobrir o conjunto exato de circunstâncias que estavam causando o problema.
fonte
Para um projeto da Universidade, estávamos escrevendo um sistema de nós P2P distribuídos que compartilham arquivos, com suporte a multicasting para detectar um ao outro, vários anéis de nós e um servidor de nomes para que um nó seja atribuído a um cliente.
Escrito em C ++, usamos o POCO para isso, pois ele permite uma boa programação de IO, Socket e Thread.
Surgiram dois bugs que nos incomodaram e nos fizeram perder muito tempo, um realmente lógico:
Aleatoriamente, um computador estava compartilhando o IP do host local em vez do IP remoto.
Isso fez com que os clientes se conectassem ao nó no mesmo PC ou nós para se conectarem.
Como identificamos isso? Quando melhoramos a saída no servidor de nomes, descobrimos mais tarde quando reiniciamos os computadores que nosso script para determinar o IP a fornecer estava errado. Aleatoriamente, o dispositivo lo foi listado primeiro em vez do dispositivo eth0 ... Realmente estúpido. Então agora nós codificamos o código para eth0, pois isso é compartilhado entre todos os computadores da universidade ...
E agora um mais irritante:
Aleatoriamente, o fluxo de pacotes seria interrompido aleatoriamente.
Quando o próximo cliente se conectar, ele continuará ...
Isso aconteceu de forma aleatória e, como mais de um computador está envolvido, é mais irritante depurar esse problema. Os computadores da universidade não nos permitem executar o Wireshark naqueles, pelo que ficamos imaginando se o problema estava no lado de envio ou no recebimento. lado.
Com muita saída no código, assumimos apenas que o envio dos comandos funciona bem,
isso nos deixou imaginando onde estava o problema real ... Parecia que a maneira como o POCO pesquisa estava errado e que deveríamos procurar caracteres disponíveis na tomada de entrada.
Partimos do pressuposto de que isso funcionava como testes mais simples em um protótipo envolvendo menos pacotes não causava esse problema, então isso nos levou a supor que a declaração da pesquisa estava funcionando, mas ... Não estava. :-(
Lições aprendidas:
Não faça suposições estúpidas como a ordem dos dispositivos de rede.
As estruturas nem sempre realizam seu trabalho (implementação ou documentação).
Forneça saída suficiente no código, se não for permitido, certifique-se de registrar detalhes estendidos em um arquivo.
Quando o código não foi testado (porque é muito difícil), não assuma que as coisas funcionem.
fonte
Eu ainda estou na minha caçada mais difícil. É um daqueles que às vezes está lá e às vezes não é bugs. É por isso que estou aqui, às 6h10 do dia seguinte.
Fundo:
A Caça.
A matança.
Post-mortem.
fonte
Eu tive que consertar algumas coisas de concorrência confusas no último semestre, mas o bug que ainda mais se destaca foi em um jogo baseado em texto que eu estava escrevendo na montagem do PDP-11 para uma tarefa de casa. Era baseado no Jogo da Vida de Conway e, por algum motivo estranho, grande parte da informação ao lado da grade era constantemente substituída por informações que não deveriam estar lá. A lógica também era bastante direta, por isso era muito confusa. Depois de revisar várias vezes para redescobrir que toda a lógica está correta, repentinamente notei qual era o problema. Essa coisa:
.
No PDP-11, esse pequeno ponto próximo a um número faz com que seja a base 10 em vez de 8. Era próximo a um número que delimitava um loop que deveria ser limitado à grade, cujo tamanho foi definido com os mesmos números, mas na base 8)
Ele ainda se destaca por causa da quantidade de danos causados por uma adição tão pequena do tamanho de 4 pixels. Então, qual é a conclusão? Não codifique no conjunto PDP-11.
fonte
Programa de quadro principal parou de funcionar sem motivo
Acabei de postar isso em outra pergunta. Veja o post aqui
Isso aconteceu porque eles instalaram uma versão mais recente do compilador no quadro principal.
Atualização 06/11/13: (A resposta original foi excluída pelo OP)
Herdei esse aplicativo de quadro principal. Um dia, do nada, parou de funcionar. É isso aí ... só parou.
Meu trabalho era fazê-lo funcionar o mais rápido possível. O código fonte não foi modificado por dois anos, mas de repente parou. Tentei compilar o código e ele quebrou na linha XX. Olhei para a linha XX e não sabia dizer o que faria a linha XX quebrar. Eu pedi as especificações detalhadas para esta aplicação e não havia nenhuma. A linha XX não era a culpada.
Imprimi o código e comecei a analisá-lo de cima para baixo. Comecei a criar um fluxograma do que estava acontecendo. O código era tão complicado que eu mal conseguia entender. Desisti de tentar fazer um fluxograma. Eu tinha medo de fazer alterações sem saber como essa alteração afetaria o restante do processo, principalmente porque não tinha detalhes do que o aplicativo fazia.
Então, decidi começar no topo do código-fonte e adicionar freios à linha branca e à espinha para tornar o código mais legível. Percebi que, em alguns casos, havia condições que combinavam ANDs e ORs e não era claramente distinguível entre quais dados estavam sendo ANDed e quais dados estavam sendo ORed. Então comecei a colocar parênteses em torno das condições AND e OR para torná-las mais legíveis.
À medida que me afastava lentamente, eu periodicamente salvava meu trabalho. A certa altura, tentei compilar o código e aconteceu uma coisa estranha. O erro saltou da linha de código original e agora estava mais abaixo. Então continuei, separando as condições AND e OR com parênteses. Quando terminei de limpá-lo, funcionou. Vai saber.
Decidi então visitar a loja de operações e perguntar se eles haviam instalado recentemente algum novo componente no chassi principal. Eles disseram que sim, atualizamos recentemente o compilador. Hmmmm.
Acontece que o compilador antigo avaliou a expressão da esquerda para a direita, independentemente. A nova versão do compilador também avaliou expressões da esquerda para a direita, mas o código ambíguo significa que a combinação pouco clara de ANDs e ORs não pôde ser resolvida.
Lição que aprendi com isso ... SEMPRE, SEMPRE, SEMPRE use parênteses para separar as condições AND e OR quando forem usadas em conjunto.
fonte
Fundo:
A Caça.
A matança.
Post-mortem.
gdb
+ monitoramento! Levamos um tempo para suspeitar do disco e, em seguida, identificar a causa dos picos de atividade no gráfico de monitoramento ...fonte
O mais difícil nunca foi morto porque nunca poderia ser reproduzido senão em todo o ambiente de produção com a fábrica em operação.
O mais louco que eu matei:
Os desenhos são impressos sem sentido!
Olho o código e não consigo ver nada. Pego um trabalho para fora da fila da impressora e o examino, parece bom. (Isso foi na época dos, PCL5 com HPGl / 2 incorporado - na verdade, muito bom para plotar desenhos e sem dores de cabeça para criar uma imagem rasterizada com memória limitada.) Dirijo-o para outra impressora que deve entendê-lo, ele imprime bem .
Reverta o código, o problema ainda está lá.
Finalmente, eu faço manualmente um arquivo simples e o envio para a impressora - sem sentido. Acontece que não foi meu bug, mas a própria impressora. A empresa de manutenção atualizou para a versão mais recente quando estavam consertando outra coisa e essa versão mais recente teve um erro. Fazer com que eles entendessem que haviam retirado a funcionalidade crítica e precisavam atualizá-la para uma versão anterior era mais difícil do que encontrar o bug.
Um que foi ainda mais irritante, mas como estava apenas na minha caixa, eu não colocaria em primeiro lugar:
Borland Pascal, código DPMI para lidar com algumas APIs não suportadas. Execute-o, ocasionalmente funcionou, geralmente deu certo tentando lidar com um ponteiro inválido. Porém, nunca produziu um resultado errado, como seria de esperar de pisar em um ponteiro.
Depuração - se eu seguisse o código, ele sempre funcionaria corretamente, caso contrário, seria tão instável quanto antes. A inspeção sempre mostrava os valores certos.
O culpado: havia dois.
1) O código da biblioteca da Borland tinha um erro grave: ponteiros em modo real estavam sendo armazenados em variáveis de ponteiro em modo protegido. O problema é que a maioria dos ponteiros no modo real tem endereços de segmento inválidos no modo protegido e, quando você tenta copiar o ponteiro, ele o carrega em um par de registradores e o salva.
2) O depurador nunca diria nada sobre uma carga tão inválida no modo de etapa única. Não sei o que ele fez internamente, mas o que foi apresentado ao usuário parecia completamente correto. Eu suspeito que ele não estava realmente executando a instrução, mas sim simulando-a.
fonte
Este é apenas um bug muito simples que, de alguma forma, me transformei em um pesadelo.
Antecedentes: eu estava trabalhando para criar meu próprio sistema operacional. A depuração é muito difícil (instruções de rastreamento são tudo o que você pode ter e, às vezes, nem isso)
Bug: Em vez de fazer duas opções de encadeamento no modo de usuário, ele apresentaria uma falha de proteção geral.
A caça aos bugs: passei provavelmente uma semana ou duas tentando consertar esse problema. Inserindo instruções de rastreamento em todos os lugares. Examinando o código de montagem gerado (do GCC). Imprimindo todo e qualquer valor que pude.
O problema: em algum lugar no início da busca de bugs, eu havia colocado uma
hlt
instrução no crt0. O crt0 é basicamente o que inicializa um programa do usuário para uso em um sistema operacional. Estahlt
instrução causa um GPF quando executado no modo de usuário. Coloquei lá e basicamente esqueci. (originalmente o problema era algo como um estouro de buffer ou erro de alocação de memória)A correção: remova a
hlt
instrução :) Depois de removê-la, tudo funcionou bem.O que eu aprendi: Ao tentar depurar um problema, não perca o controle das correções que você tenta. Faça diffs regulares em relação à versão estável mais recente do controle de origem e veja o que você mudou recentemente quando nada mais funciona
fonte