Rodando testes antes do commit em Mercurial

outubro 12, 2011

Como todo mundo, entrei de cabeça na onda dos sistemas de versão distribuídos, como Git. Por várias razões, porém, meu DVCS “do coração” é Mercurial. (Razões as quais pretendo explicar em breve, por sinal).

De qualquer forma, vai aí minha primeira dica sobre o Mercurial. Frequentemente, estou consertando um bug em um projeto…

$ nano module1.c

…e, como uso TDD, rodo os testes:

$ make test
./run_all
................................................................................

OK (80 tests)

Quando os testes passam e o bug está corrigido, comito o código alterado:

$ hg commit -m "Bug #123 corrected"

Daí, passo a trabalhar em outra funcionalidade no mesmo projeto, escrevendo os testes primeiro:

$ nano test/module2.c

Novamente no espírito de TDD, vou rodar os testes, para quebrarem. Aperto então Control+P (ou ) para chegar ao comando que roda os testes novamente (make test). Infelizmente, porém, às vezes eu aperto Enter muito cedo, o que resulta em comitar minhas alterações recentes:

$ hg commit -m "Bug #123 corrected"

Isto é ruim porque cria uma versão com código quebrando no Mercurial. A solução paliativa é executar hg rollback (obrigado de novo, Stack Overflow!). Entretanto,  hg rollback é a pílula do dia seguinte do Mercurial: envolve vários riscos e deve ser usado com cuidado.

Então tive um estalo: por que não rodo os testes sempre antes do commit? A resposta é que eu comito por acidente, claro, mas posso fazer o Mercurial rodá-los antes de confirmar um commit: bastaria criar um hook. Para isto, alterei o programa que roda os testes para retornar um valor diferente de 0 (zero) quando os testes falhassem. Assim make test retorna um valor diferente de zero. Antes eu tinha algo assim:

void RunAllTests(void) {
    CuString *output = CuStringNew();
    CuSuite* suite = CuSuiteNew();
    CuSuiteAddSuite(suite, test_project_suite());
    // ... mais coisas aqui
    CuSuiteRun(suite);
    CuSuiteSummary(suite, output);
    CuSuiteDetails(suite, output);
    printf("%s\n", output->buffer);
    CuStringDelete(output);
    CuSuiteDelete(suite);
}
int main(void) {
    RunAllTests();
    return 0;
}

Agora eu tinha algo assim:

int RunAllTests(void) { // Retorna int ao invés de void
    CuString *output = CuStringNew();
    CuSuite* suite = CuSuiteNew();
    CuSuiteAddSuite(suite, test_project_suite());
    // ... mais coisas aqui
    CuSuiteRun(suite);
    CuSuiteSummary(suite, output);
    CuSuiteDetails(suite, output);
    printf("%s\n", output->buffer);
    CuStringDelete(output);
    CuSuiteDelete(suite);
    return suite->failCount; // Retorna contagem de erros
}
int main(void) {
    return RunAllTests(); // Retorna contagem
}

(Caso você esteja se perguntando, estou utiliando CuTest, o melhor e mais cacofônico framework de testes para C.)

Após fazer esta alteração, adicionei as linhas abaixo no arquivo .hg/hgrc do projeto:

[hooks]
pretxncommit.surefire = make test

O que acontece quando vou comitar erroneamente agora? Veja só:

$ hg commit -m "Bug #123 fixed"
cc -c  -Wall -std=c99 -Iinclude -Icutest src/test/util.c -o test_util.o
cc   run_all.o test_secretary.o  CuTest.o libsecretary.a   -o run_all
./run_all
...........................................................F....................

There was 1 failure:
1) test_util_beginning_of_day: src/test/util.c:34: expected 1 but was 0

!!!FAILURES!!!
Runs: 80 Passes: 79 Fails: 1

make: *** [test] Error 1
transaction abort!
rollback completed
abort: pretxncommit.surefire hook exited with status 2

Os testes falham, o que faz o programa que os roda retornar um valor diferente de zero. O programa, falhando, faz o make falhar, retornando também um valor diferente de zero. Como o make falhou, o hook falha também, impedindo que Mercurial siga em frente com o commit. Oras, o hook foi executado na fase pretxncommit (pre transaction commit), logo antes de Mercurial registrar o commit do código. Como o hook falhou, o commit não é efetivamente feito e meu histórico fica limpo.

Este exemplo utiliza testes escritos em C, mas serve para qualquer projeto. Eventualmente, não se pode alterar o programa que roda os testes, mas pode-se criar um script que lê a saída dos testes e retorna o valor correto.

Problemas na instalação do módulo de SNMP do Apache

março 7, 2010

Se você está tentando instalar o módulo SNMP do Apache e seguiu todas as instruções daqui, mas encontrou no error.log uma mensagem como

Cannot find module (APACHE2-MIB): At line 0 in (none)
APACHE2-MIB::serverName.0: Unknown Object Identifier
APACHE2-MIB::serverTmpDir.0: Unknown Object Identifier
APACHE2-MIB::agentHttpAddress.0: Unknown Object Identifier
APACHE2-MIB::serverStatus.0: Unknown Object Identifier
APACHE2-MIB::serverVersion.0: Unknown Object Identifier
APACHE2-MIB::serverBuilt.0: Unknown Object Identifier
APACHE2-MIB::serverRoot.0: Unknown Object Identifier
APACHE2-MIB::serverPidfile.0: Unknown Object Identifier
APACHE2-MIB::serverRestart.0: Unknown Object Identifier
APACHE2-MIB::totalServerPorts.0: Unknown Object Identifier
APACHE2-MIB::serverPortNumber.1: Unknown Object Identifier

provavelmente o MIB do Apache não está onde devia.

Dentro do diretório onde você compilou o mod-ap2-snmp, haverá um subdiretório chamado mib. Dentro dele, haverá um arquivo chamado APACHE2-MIB.TXT. Copie esse arquivo para o diretório dos MIBs do Net-SNMP. (Na minha máquina, esse diretório era /usr/local/share/snmp/mibs/)

Dois detalhes:

  • a extrensão do arquivo deve estar em minúsculas – então a cópia se chamará APACHE2-MIB.txt. Não sei por que o pessoal do mod-ap2-snmp colocou esse nome com extensão em maiúscula, deve ser bug.
  • o arquivo deve ser legível para todos: rode chmod a+r /usr/local/share/snmp/mibs/APACHE2-MIB.txt nele. Talvez essa seja uma permissão muito aberta e só um grupo precise vê-lo, mas eu a apliquei e o snmpget conseguiu lê-lo. Sugestões sobre permissões mais seguras são bem-vindas, mas não sei se é necessário restringir a visibilidade desse arquivo…

Esses passos resolveram as mensagens acima. Sobraram essas:

[Thu Mar 04 00:16:41 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:41 2010] [error] APACHE2-MIB::serverStatus.0
[Thu Mar 04 00:16:42 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:42 2010] [error] APACHE2-MIB::serverName.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverName.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverTmpDir.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::agentHttpAddress.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverStatus.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverVersion.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverBuilt.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverRoot.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverPidfile.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverRestart.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::totalServerPorts.0
[Thu Mar 04 00:16:43 2010] [error] AP2_SNMP: Can't set MIB value.
[Thu Mar 04 00:16:43 2010] [error] APACHE2-MIB::serverPortNumber.1

O problema aqui é que iniciei o Apache antes de iniciar o agente snmp (snmpd). Basta derrubar o apache, levantar o agente (no caso, na minha máquina, ele está em /usr/local/sbin/snmpd) e levantar novamente o Apache. Feito isso, comece a recuperar as informações!

$ snmpget -v 3 -u usuario -l authNoPriv -a MD5 -A senha  localhost APACHE2-MIB::serverName.0
APACHE2-MIB::serverName.0 = STRING: 127.0.1.1

Note, porém, que são necessários usuário e senha para trabalhar com SNMPv3 – que é o padrão, ao que parece, para o mod-ap2-snmp e é o melhor protocolo, de qualquer forma. Para criá-los, veja esse link.

Congelamento de disco rígido

setembro 17, 2009

Afirma a lenda que, se seu disco rígido parar de funcionar, você ainda terá a oportunidade de recuperar dados importantes congelando-o. Mas será verdade?

Tive a oportunidade de conferir. Depois de três dias trabalhando, fui tentar enviar minhas alterações de um projeto ao Subversion. O servidor estava fora do ar, mas não era um grande problema: eu poderia enviar as alterações no dia seguinte. Chego no dia seguinte no trabalho para enviar os dados e… bem, o HD da minha máquina havia pifado.

Fria: um de nós dois teria de entrar em uma

Fria: um de nós dois teria de entrar em uma

Depois de dias tentando recuperar os dados do HD, desisti. Seria mais fácil reescrever tudo o que eu havia feito, pelo visto. Já aceitando meu destino, fui fazer outras tarefas.

Algum tempo depois, encontro um amigo meu. Aproveito, conto minha história desinteressadamente, como uma curiosidade. Meu amigo me pergunta:

Já tentou congelar o HD?

Já havia ouvido falar desta técnica. Se o seu disco rígido parou de funcionar. tente colocá-lo no congelador. Esse meu amigo sempre garantiu que o truque funcionava, mas nunca vi ninguém fazê-lo…

Pois bem, como pior não poderia ficar, resolvi tentar. Peguei o HD, embrulhei-o muito bem em uma sacola plástica, pus no congelador e saí para trabalhar em outro projeto, em outro lugar.

Ao final da tarde, voltei à empresa. Tirei o disco rígido do congelador, coloquei-o no notebook e liguei a máquina com um CD do Ubuntu. Quando fui ver… funcionou! Consegui acessar meus arquivos, e os copiei para outra máquina com scp!

Se você for tentar fazer isso algum dia, tome cuidado de embrulhar muito bem o disco, para que não molhe. Note que isto provavelmente não deve funcionar sempre… mas funcionou uma vez, isto posso garantir!

Fazendo DMA funcionar no Debian/Ubuntu

fevereiro 21, 2009

Até o meio do ano passado, meu computador era um Athlon XP 2.3 GHz com 128 MB de memória e 40 GB de HD. Fraquinho, mas funcionava até bem. Eu continuaria utilizando-o cotidianamente se não tivesse de utilizar o OpenOffice.org e o Eclipse. Além disso, estava querendo brincar com outros sistemas operacionais, e preferia virtualizá-los. Isso obviamente era inviável na máquina antiga.

Vendo que não havia mais para onde escapar, comprei outro computador. O computador era de um amigo meu, tinha 80 GB de HD, 1 GB de memória, gravadora de DVD e uma placa GForce 2200, ou algo assim. É uma máquina ótima para meus objetivos. Formatei as partições, instalei um Debian e configurei a máquina.

Notei, porém, que o computador estava bastante lento, muito mais lento que minha máquina antiga. Depois de pesquisar, descobri que era um problema com o HD e DMA. O syslog estava cheio de mensagens como:

Apr 8 00:17:41 localhost kernel: ide: failed opcode was: unknown
Apr 8 00:17:47 localhost kernel: hda: status timeout: status=0xd0 { Busy }
Apr 8 00:17:47 localhost kernel:
Apr 8 00:17:47 localhost kernel: ide: failed opcode was: unknown
Apr 8 00:17:47 localhost kernel: hdb: DMA disabled
Apr 8 00:17:47 localhost kernel: hda: drive not ready for command
Apr 8 00:17:47 localhost kernel: ide0: reset: success
Apr 8 00:14:07 localhost kernel: hdb: dma_timer_expiry: dma status == 0x41
Apr 8 00:14:07 localhost kernel: hdb: DMA timeout error
Apr 8 00:14:07 localhost kernel: hdb: dma timeout error: status=0x58 { DriveReady SeekComplete DataRequest }

Aparentemente, havia algum erro ao carregar os módulos do DMA. Pesquisando sobre o problema, vi sugestões para verificar a saída do comando hdparm -i /dev/hda. O resultado foi algo como:

/dev/hda:

Model=SAMSUNG SP0802N, FwRev=TK100-24, SerialNo=S00JJ10XB83245
Config={ HardSect NotMFM HdSw>15uSec Fixed DTR>10Mbs }
RawCHS=16383/16/63, TrkSize=34902, SectSize=554, ECCbytes=4
BuffType=DualPortCache, BuffSize=2048kB, MaxMultSect=16, MultSect=off
CurCHS=16383/16/63, CurSects=16514064, LBA=yes, LBAsects=156365903
IORDY=on/off, tPIO={min:240,w/IORDY:120}, tDMA={min:120,rec:120}
PIO modes:  pio0 pio1 pio2 pio3 pio4
DMA modes:  mdma0 mdma1 mdma2
UDMA modes: udma0 udma1 udma2 udma3 udma4 *udma5
AdvancedPM=no WriteCache=enabled
Drive conforms to: ATA/ATAPI-7 T13 1532D revision 0:  ATA/ATAPI-1,2,3,4,5,6,7


* signifies the current active mode

Em resumo, a linha

UDMA modes: udma0 udma1 udma2 udma3 udma4 *udma5

indicava que o módulo do DMA estava carregado! O que, então, estava dando errado?

Em uma breve pesquisa no Google, encontrei essa thread nos Ubuntu Forums. Nela, alguem sugere verificar se o HD estava configurado para Master ao invés de Cable Select. Meu HD já estava jumpeado e cabeado como Master. Todas as outras soluções que encontrei não funcionaram para ninguém – inclusive, não funcionavam para mim. O que fazer?

Pois bem, em um experimento, eu coloquei o HD como Secondary Master; antes, reconfigurei o GRUB para que o root do kernel passasse a ser /dev/hdc1 e editei o /etc/fstab trocando /dev/hda por /dev/hdc (e vice-versa). Liguei a máquina e… Voi là! O problema sumiu!

Hoje, porém, fui tentar configurar o HD como Primary Master e pesquisar por uma solução menos estranha. Infelizmente, tive novamente o mesmo problema com DMA, não importasse o que eu fizesse. Desisti e voltei o HD para Secondary Master – afinal, estava só explonrando possibilidades. Quando reinicio a máquina, o erro de DMA reaparece, agora com o HD como Secondary Master! Desligo então a máquina, tiro e recoloco o cabo flat no HD e reinicio a máquina. Voi là!2 o problema foi resolvido novamente.

Então, se você está tendo esse problema, tente tirar e recolocar o cabo flat, que pode estar frouxo. Se isso não funcionar, dá uma olhada na thread citada, que ela tem boas dicas. Entretanto, se ela não te ajudar, tente colocar o HD como Secondary Slave (ou como Master Slave, se já estiver como Secondary Slave). Vai que funciona, não é?

HTH. Até mais!

Dividindo uma imagem em várias páginas com ImageMagick

novembro 19, 2008

Esses dias, um amigo me pergunta no Google Talk:

duvida de linux
tenho uma imagem
bem grande
e quero que ela seja impressa
em varias paginas
tipo um pedaco numa pagina
outro pedaco em outra

(Antes de prosseguir, uma nota: uma imagem nos formatos JPEG, PNG etc. não possui um tamanho, mas sim uma resolução. Assim, você pode exibir a imagem em qualquer tamanho, mas a resolução provavelmente vai impor limites à qualidade da imagem. Do mesmo modo, uma imagem de, digamos 1900 x 1200 pixels não tem um tamanho definido, mas sim uma resolução, e pode ser impressa tanto numa folha A3, folha A4 ou qualquer outra, variando apenas a qualidade da impressão.)

Quando alguém me fala de processar imagens no Linux, a primeira coisa que me vem a mente é a suíte ImageMagick. Entre as ferramentas do ImageMagick, a que mais uso é o convert, um programa de linha de comando que permite executar inúmeras operações sobre imagens, como converter de formato, redimensionar, gerar negativo, extrair um pedaço etc. etc.

A minha abordagem seria, então, dividir a imagem em pequenas imagens contíguas e retangulares. Para imprimir numa folha A4, por exemplo, as pequenas imagens deveriam ter proporções de uma folha A4. Como fazer isso?

O primeiro passo é descobrir como recuperar um retângulo de uma imagem. Isto é bem simples com o convert, basta utilizar a opção -crop. Para nossa missão, nós usaremos essa opção com uma string na forma

<width>x<height>+<x>+<y>

onde <width> é a largura da imagem resultante, em pixels; <height> é a altura da imagem resultante, em pixels; <x> e <y> são as coordenadas do pixel a partir de onde a imagem será cortada. Desse modo, se quisermos extrair um retângulo de 100 x 100 pixels de uma figura no arquivo lena.png de 512 x 512 pixels a partir do centro, faríamos algo como

convert -crop 100x100+256+256 lena.png output.png

Se a imagem for essa:

Lena Söderberg, SFW

Lena Söderberg, SFW

o resultado do comando acima será:

Resultado do corte da imagem

Resultado do corte da imagem

(Note que a opção -crop pode ser utilizada de outras maneiras. Confira na documentação da opção.)

Agora, precisamos gerar várias imagens a partir da primeira. Para não ficar fazendo isso na mão, podemos usar o comando for junto com o comando seq. (Se você não sabe usar o comando for e o comando seq do bash, essa página explica muito bem como funcionam.) Desse modo, se eu quisesse dividir a imagem da Lena acima em retângulos de 100 x 200 pixels, eu faria algo como:

for i in `seq 0 $((512/100))`; do
    for j in `seq 0 $((512/200))`; do
        convert -crop 100x200+$((i*100))+$((j*200)) \
            lena.png lena-$j-$i.png
    done
done

Estou dividindo a imagem numa planilha de imagens. Para cada linha, eu vou gerar 512/100 +1 = 6 imagens; para cada coluna, eu vou gerar 512/200 +1 = 3 imagens. (Se você não entendeu o “+1”, lembre-se que estamos contando a partir de zero, como em C, Java etc). A primeira imagem será o retângulo que vai do pixel de coordenada (0, 0) até o pixel de coordenada (100, 200); a segunda imagem irá do pixel de coordenada (100, 0) até o pixel de coordenada (200, 200); do mesmo modo, a primeira imagem da linha abaixo irá do pixel de coordenada (0, 200) até o pixel de coordenada (100, 400) etc. etc., contando as coordenadas a partir do canto superior esquerdo.

Ao rodar isso, gerei dezoito imagens. O resultado, que uni em uma imagem só por praticidade, pode ser visto abaixo. As linhas brancas separam as imagens geradas.

A borda branca separa as inúmeras imagens que foram geradas

Lena, dividida

(Note como o convert, ao encontrar um retângulo com um pedaço vazio, gera a maior figura possível. Isso pode ser notado nas bordas direita e inferior.)

Vamos generalizar o algoritmo. Faremos um script que recebe como parâmetro as dimensões originais do arquivo, as dimensões das imagens a serem geradas e o nome do arquivo original. O resultado será algo como

file=$1
originalx=$2
originaly=$3
slicex=$4
slicey=$5
numberx=$((originalx/slicex))
numbery=$((originaly/slicey))

for i in `seq 0 $numberx`; do
  for j in `seq 0 $numbery`; do
    convert -crop ${slicex}x${slicey}+$((i*slicex))+$((j*slicey)) \
        $file $file-$j-$i.png
  done
done

Agora, é só rodar o script dando como argumentos as dimensões originais e algumas dimensões proporcionais ao papel que queremos utilizar. Obteremos imagens que caberão perfeitamente no papel. (Vale lembrar que uma imagem não tem um tamanho em si, mas uma resolução: a qualidade final pode não ficar muito boa, dependendo da resolução da sua imagem.)

Para facilitar o trabalho, vamos juntar todas as imagens em um único arquivo PDF, o que facilitaria a impressão. O convert do ImageMagick pode fazer isso de maneira bem simples: se invocarmos o  convert passando como parâmetro uma série de imagens e, ao final, o nome de um arquivo com extensão .pdf, o resultado será um arquivo PDF com uma imagem por página. Assim sendo, ao chamar algo como

convert fig1.png fig2.png fig3.png one-per-page.pdf

one-per-page.pdf vai conter, ao vai conter, ao final, três páginas. Na primeira, estará fig1.png; na segunda, teremos fig2.png e, na terceira página, estará fig3.png.

Assim, vamos complementar o script criando uma variável que armazena o nome de todos os arquivos gerados (separados por um espaço em branco). Após gerar todas as imagens, vamos colocá-las todas em um arquivo PDF. O script final, você pode vê-lo no pastebin.

Eu apliquei o script sobre nossa imagem, usando as dimensões proporcionais a papel A4 (210 x 297 mm):

./split.sh lena.png 512 512 210 297

O resultado pode ser encontrado aqui. As páginas acabaram em formato A7, mas, se mandar imprimir, elas preencherão toda a folha A4 sem problemas.

O script está disponível para quem quiser fazer qualquer uso dele. Ademais, pode ser melhorado: é possível, por exemplo, fazer com que o ImageMagick descubra ele mesmo as dimensões iniciais da imagem. Entretanto, acredito que ele já possa ser bem útil

Obrigado, Renan Mendes, pela dica sobre como gerar PDFs. A todos, até mais!

Módulos do VirtualBox no Debian Lenny

novembro 12, 2008

Eu estava tentando usar o VirtualBox no meu computador de casa, que roda Debian Lenny. No começo, até funcionou, mas o kernel Linux foi atualizado em algum momento, e os módulos do VirtualBox pararam de funcionar.

Instalei todos os módulos possíveis do repositório, mas o VirtualBox se recusava a funcionar. Procuro no Google alguma solução, e não encontro nada, exceto que devo recompilar os módulos. Eu realmente não estava disposto a fazer isso…

Entretanto, pesquisa vai, pesquisa vem, encontrei uma solução melhor. Envolve compilar os módulos também, mas de maneira mais “debiana”.

Para compilar os módulos, primeiro, atualize a referência aos pacotes com os repositorios:

# apt-get update

Agora, atualize os pacotes instalados em sua máquina:

# apt-get upgrade

Feito isso, instale o pacote com o código-fonte dos módulos do VirtualBox:

# apt-get install virtualbox-ose-source

O pulo do gato é utilizar a ferramenta module-assisant para compilar o módulo. Uma vez que o código-fonte do módulo esteja instalado, basta executar:

# m-a a-i virtualbox-ose

O m-a compila o módulo. A opção a-i diz ao module-assistant para instalar os módulos automaticamente.

Voilà! Seus módulos estão funcionando. No máximo, vai precisar carregar os módulos:

# modprobe vboxdrv

Se isso resolver seu problema, agradeça ao Daniel Baumann lá do e-mail. Tudo bem que dizer que tudo isso é óbvio foi exagero dele, mas a ajuda valeu bastante 🙂

A Idéia mais Estúpida da Computação

outubro 10, 2008

Osvaldo Santana, o pythonista, postou no seu blog algumas “cagadas computacionais” que cometera. Lembrei-me de um causo interessante…

Meu primeiro emprego foi como servidor público, técnico administrativo na Universidade de Brasília. Era um emprego chato para mim, tecnocrata que sou, mas consegui escapar da chatice convencendo o pessoal a me deixar usar um Debian na minha máquina – na época, um Debian Etch, ainda em testes.

Em casa, eu já usava Debian, mas não tinha conexão com a Internet, de modo que fiquei preso ao Debian Woody, GNOME 1.8 etc. etc. No trabalho, porém, eu tinha uma ótima conexão, então atualizava freqüentemente o sistema operacional. Foi minha primeira experiência mais interativa com o APT: antes, só utilizava para instalar pacotes dos sete CDs do Woody que eu tinha gravado.

Depois de um bom tempo usando Debian, enfrento meu primeiro inferno de dependências. Bem feito, quem mandou misturar testing, unstable e até experimental, né? De qualquer forma, entrei em desespero, porque aquela era minha máquina de trabalho e tinha de funcionar. Bato a cabeça, reinstalo pacote, tiro repositório, dou apt-get update pra cá, apt-get dist-upgrade para lá mas nada se resolve…

No desespero daquela sexta feira, tomo uma decisão drástica: vou desinstalar o APT! Lá vamos nós digitar o inacreditável comando:

apt-get remove apt

O Debian não gostou muito… A mensagem que recebi foi algo como:

AVISO: Os pacotes essenciais a seguir serão removidos.
Isso NÃO deveria ser feito a menos que você saiba exatamente o que você está fazendo!
apt
Depois desta operação, 30,1MB de espaço em disco serão liberados.
Você está prestes a fazer algo potencialmente destrutivo.
Para continuar digite a frase 'Sim, faça o que eu digo!'
?]

Bem, é realmente uma mensagem assustadora. Qualquer pessoa perceberia que desinstalar o APT não era só aparentemente uma idéia sem sentido, era um absurdo estúpido! Mas eu não sou qualquer pessoa! Como sou brasileiro e não aprendo nunca, vou lá e digito:

Sim, faça o que eu digo!

Depois de todo o trabalho sujo feito, vamos tentar reinstalar o APT. Bem, vocês podem imaginar que não, não consegui fazer isso. Quando vi que a opção menos absurda seria recompilar o APT, desisti: fiz backup dos documentos e reinstalei o Debian.

Pelo menos saí do inferno de dependências 🙂

Tratamento de erros em C com goto

setembro 13, 2008

Esses dias, começou-se a discutir na lista de discussão da Python Brasil razões para se utilizar exceções. Em um certo momento, um participante reconhecidamente competente comentou o quanto é difícil tratar erros através do retorno de funções, como em C.

Quando se tem um algoritmo complexo, cada operação passível de erro implica em uma série de ifs para verificar se a operação ocorreu corretamente. Se a operação tiver falhado, será necessário reverter todas as operações anteriores para sair do algoritmo sem alterar o estado do programa.

Vejamos um exemplo. Suponha que eu tenha a segunte struct para representar arrays:

typedef struct {
        int size;
        int *array;
} array_t;

Agora, eu vou fazer uma função que lê, de um arquivo texto, o número de elementos a ser posto em um desses arrays e, logo em seguida, os elementos. Essa função também vai alocar a struct do array e o array de fato. O problema é que essa função é bastante propensa a erros, pois podemos não conseguir

  • abrir o arquvo dado;
  • alocar a struct;
  • ler o número de elementos do arquvo dado, seja por erro de entrada/saída, seja por fim do arquivo;
  • alocar memória para guardar os elementos a serem lidos;
  • ler um dos elementos, seja por erro de entrada/saída, seja por fim do arquivo.

Complicado, né? Note que, se conseguirmos abrir o arquivo mas não conseguirmos alocar a struct, temos de fechar o arquivo; se conseguirmos abrir o arquivo e alocar a struct mas não conseguirmos ler o número de elementos do arquivo, temos de dealocar a struct e fechar o arquivo; e assim por diante. Assim sendo, se verificarmos todos os erros e adotarmos a tradição de, em caso de erro, retornar NULL, nossa função seria mais ou menos assim:

array_t *readarray(const char *filename) {
        FILE *file;
        array_t *array;
        int i;

        file = fopen(filename, "r");
        if (file == NULL) return NULL;

        array = malloc(sizeof(array_t));
        if (array == NULL) {
		fclose(file);
		return NULL;
	}

        if (fscanf(file, "%d", &(array->size)) == EOF) {
		free(array);
		fclose(file);
		return NULL;
	}

        array->array = malloc(sizeof(int)*array->size);
        if (array->array == NULL)  {
		free(array);
		fclose(file);
		return NULL;
	}

        for (i = 0; i < array->size; i++) {
                if (fscanf(file, "%d", array->array+i) == EOF) {
			free(array->array);
			free(array);
			fclose(file);
			return NULL;
		}
        }
        return array;
}

De fato, bastante trabalhoso, e com muito código repetido…

Note, porém, como há duas situações no código acima. Em uma, quando tenho duas operações para reverter, preciso reverter primeiro a última executada, e depois a anterior. Por exemplo, quando vou dealocar tanto a struct quanto o array de inteiros, preciso dealocar primeiro o array de inteiros e depois a struct. Se dealoco a struct primeiro. posso não conseguir dealocar o array posteriormente.

Na outra situação, a ordem não importa. Por exemplo, se vou dealocar a struct e fechar o arquivo, não importa em que ordem eu o faça. Isso implica que eu posso, também, reverter primeiro a última operação executada e depois a primeira operação.

Qual o sentido disso? Bem, na prática, nunca vi uma situação onde eu tenha de reverter primeiro a primeira operação executada, depois a segunda e assim por diante. Isso significa que, quando faço as operações a(), b(), c() etc. a maneira “natural” de revertê-las é chamando os reversores de trás para frente, mais ou menos como:

a();
b();
c();
/* ... */
revert_c();
revert_b();
revert_a();

Agora, vem o pulo do gato. No código acima, após cada operação, vamos colocar um if para verificar se ela falhou ou não. Se falhou, executar-se-á um goto para o reversor da última operação bem sucedida:

a();
if (failed_a()) goto FAILED_A;
b();
if (failed_b()) goto FAILED_B;
c();
if (failed_c()) goto FAILED_C;
/* ... */
revert_c();
FAILED_C:
revert_b();
FAILED_B:
revert_a();
FAILED_A:
return;

Se  a() falhar, o algoritmo retorna; se  b() falhar, o algoritmo vai para FAILED_B:, reverte  a() e retorna; se c() falhar, o algoritmo vai para FAILED_C, reverte b(), reverte  a() e retorna. Consegue ver o padrão?

Pois bem, se aplicarmos esse padrão à nossa função readarray() o resultado será algo como:

array_t *readarray(const char *filename) {
        FILE *file;
        array_t *array;
        int i;

        file = fopen(filename, "r");
        if (file == NULL) goto FILE_ERROR;

        array = malloc(sizeof(array_t));
        if (array == NULL) goto ARRAY_ALLOC_ERROR;

        if (fscanf(file, "%d", &(array->size)) == EOF)
                goto SIZE_READ_ERROR;

        array->array = malloc(sizeof(int)*array->size);
        if (array->array == NULL) goto ARRAY_ARRAY_ALLOC_ERROR;

        for (i = 0; i < array->size; i++) {
                if (fscanf(file, "%d", array->array+i) == EOF)
                        goto ARRAY_CONTENT_READ_ERROR;
        }
        return array;

        ARRAY_CONTENT_READ_ERROR:
        free(array->array);
        ARRAY_ARRAY_ALLOC_ERROR:
        SIZE_READ_ERROR:
        free(array);
        ARRAY_ALLOC_ERROR:
        fclose(file);
        FILE_ERROR:
        return NULL;
}

Quais as vantagens desse padrão? Bem, ele reduz a repetição de código de reversão de operações e separa o código de tratamento de erro da lógica da função. Na verdade, apesar de eu achar exceções o melhor método de tratamento de erros moderno, para tratamento de erros in loco (dentro da própria função) eu acho esse método muito mais prático.

Very funny.vbs

julho 11, 2008

Hoje, recebo o seguinte e-mail de um amigo que trabalha com Visual Basic:

O Belo (e possivelmente bons conselhos, no geral, de como fazer código bom):
http://www.visibleprogress.com/vb_error_handling.htm

e o Horroroso
http://blogs.msdn.com/ericlippert/archive/2004/09/09/227461.aspx

O primeiro link realmente tem sugestões boas e interessantes para quem programa em Visual Basic. O segundo é uma lista de erros que podem ocorrer em Visual Basic e VBScript. O erro que me chamou a atenção foi

48 Error in loading DLL

Erros em carregamento de DLL em Visual Basic não são novidade; o que me surpreendeu é que esse erro também pode ocorrer com VBScript.

O ILOVEYOU, senhores, era mais que inevitável: era praticamente o resultado das leis da Fisíca.

Eu amo a GVT

maio 21, 2008

Ontem, estava redigindo uma primeira versão da minha monografia de conclusão de curso. Lá pelas 14 h, vou fazer uma pesquisa no Google e, pimba! percebo que estou sem conexão com a Internet. Não me preocupei: meu modem é especiamente temperamental e, quando esquenta demais, trava. Basta reiniciá-lo que tudo volta ao normal.

Pois bem, reiniciei o modem, mas não consegui recuperar a conexão. Imaginei que o modem estava tão superaquecido que seria necessário esperar um pouco mais para que ele destravasse – embora os LEDs indicassem perfeito funcionamento.

Fui fazer outras atividades que estavam pendentes fora do computador. Duas ou três horas depois, volto para tentar reconectar. Ligo o modem, reinicio, utilizando poff e pon, o daemon pppd e… não funciona. Nesse momento, apelei e fiz aquilo que todo usuário de Linux considera uma vergonha no currículo: reiniciei o computador… Ok, só os usuários mais pedantes consideram isso uma vergonha, mas o que importa é que mesmo isso não funcionou.

Evidentemente, havia algum problema com a comunicação com o servidor. Fui fazer uns testes. Primeiro, olhei os logs do pppd com o comando plog. Noto que o pppd consegue receber um endereço IP externo, e recebe também os endereços dos servidores DNS da GVT. Tento pingar os servidores DNS mas eles não respondem! Isso tinha cara de problema de autenticação. Para verificar, comentei a linha com meu login e senha no arquivo de autenticação do pppd e reiniciei o pppd novamente. Agora, a saída do plog indicava que não consegui um endereço IP nem o endereço dos servidores DNS por falha na autenticação. Imaginei: “É, o problema não é autenticação… ao menos até esse ponto”.

Joguei a toalha e resolvi ligar para a GVT. Depois de percorrer o caminho padrão, fui atendido por um rapaz. Explico para ele a situação, falo que já reiniciei tanto o modem quanto o computador e até comento que enxergo o endereço dos servidores DNS mas não consigo pingá-los. A primeira surpresa agradável foi que o atendente não pediu para eu reiniciar o computador e o modem de novo! Ele foi verificar se havia algo de errado com minha configuração e… pimba! de novo: minha porta estava bloqueada.

Aí que me toquei: eu havia pago a fatura do mês passado na sexta-feira anterior, com seis dias de atraso. Ocorre que eu não havia recebido a fatura por correio, de modo que tive de buscá-la no site da GVT; entretanto, levei um bom tempo para me tocar que a fatura não havia chegado.

Explico-lhe a minha situação. Ele me transfere, então para o departamento de cobranças. Sem precisar esperar mais que meio minuto, sou atendido por uma simpática moça. Explico novamente a situação, ao que ela vai verificar o estado da minha conta. De fato, consta como não paga, e a moça me explica que há o prazo de 72 horas para que o pagamento conste nos servidores. Apesar de ser uma situação chata, concordo que é bem natural…

Aceito, então, a triste sina de esperar até a segunda-feira para ter minha conexão de volta… A atendente, porém, diz que pode solicitar a abertura de minha porta no mesmo momento. Oras, que ótimo! Ela solicita o serviço e diz que, em duas horas, no máximo, eu estaria com minha conexão restaurada. Peço para ela verificar o valor da última fatura, pois eu temia ter pago a fatura errada… mas felizmente paguei a correta.

Por fim, a atendente pede para eu responder uma pesquisa sobre o atendimento. Eu não poderia negar isso a ela, afinal, e lá vou eu, feliz, responder a questão. Naturalmente, disse estar plenamente satisfeito. De qualquer forma, fui lavar umas roupas, para esperar o tempo passar e, na volta, a conexão estava perfeita!

Encontrei alguns bugs no processo. Por exemplo, tive de digitar meu número de telefone no começo, por solicitação do software, pois estava falando pelo celular; entretanto, os dois atendentes me pediram meu número. Entretanto, esses são detalhes insignificantes.

A GVT nunca me causou dores de cabeça. Nunca me trataram como um criminoso por usar Linux – muito pelo contrário, seus manuais já prevêem o uso de Linux. No final de 2007, me ligaram para me fazer uma oferta que pode ser resumida assim:

Notamos que você usa pouco telefone e muita conexão. Nós podemos reduzir sua franquia e quadruplicar sua velocidade de conexão, mas você terá de pagar R$ 5,00 a menos.

Agora, pela primeira vez, precisei de seu atendimento – por um problema que, no fundo, eu mesmo causei – e fui perfeitamente atendido. Por tudo isso, aí vai o meu conselho: se você não usa os serviços da GVT acesse o www.gvt.com.br e contrate-os agora. Talvez você passe pelo único problema sério da GVT, que é a cobertura mais ou menos limitada e as longas filas de espera. Eu garanto: esse problema, vale a pena suportar.