nov 2007 11

Ontem reencontrei os amigos Sandro e Mariana que passam o fim de semana em Brasília. Combinamos uma sessão de “filme horrível” na casa do nosso querido Mateus Tormin e, acreditem, o filme que Mateus escolheu era deveras horrível. Bem, acontece que Sandro e eu fundamos o Perl Mongers Brasilia em 1999, que era na verdade uma desculpa pra tomar cerveja e falar de Perl, e ontem o Sandro chamou minha atencão porque há tempos neste blog, segundo ele, “você só fala em Hugo Chavez e CPMF e onde diabos está o Perl!!??”. Claro que ele está com a razão e hoje decidí tentar recomecar a secão Perl aqui do blog que tem apenas uns 3 posts….

O assunto deste post é o operador de teste de arquivo -M

Ao trabalhar com o operador -M $ARQUIVO para obter o número de dias desde que $ARQUIVO foi alterado, deve-se observar que a forma como Perl trabalha com esse operador não é exatamente intuitiva.

Quando você testa um arquivo com -M, espera obter o tempo desde que foi alterado até o instante em que o operador -M é utilizado. Mas não é isso que acontece.

Perl irá lhe retornar o tempo de modificacão do arquivo até o momento em que o programa foi executado.

Se o seu programa utiliza o operador -M e é um servidor de rede, ou daemon local, ou qualquer processo que permaneca em funcionamento durante longos períodos, o resultado pode ser um desastre.

Veja o seguinte exemplo:


#!/usr/bin/perl

$data = -M "/etc/passwd";
print "$data\n";
sleep(30);

$data = -M "/etc/passwd";
print "$data\n";

Normalmente esperaríamos que a segunda chamada a -M, após 30 segundos, retornasse uma fracão do dia 30 segundos maior(lembre-se, -M não retorna segundos como time(), e sim o número de dias e a fracão do dia atual desde que o arquivo testado foi alterado).

Rodando o programa acima vemos que os 30 segundos não influenciam em nada o resultado de -M


root@clapton:~/perl# perl modificado.pl
514.301099537037
514.301099537037

Vejamos alguns casos em que isso pode causar problemas:

1) Voce utiliza -M para decidir se um arquivo de log deve ser arquivado e um novo criado(estilo logrotate). Neste caso o arquivo irá crescer infinitamente pois seu programa não verá qualquer diferenca na data de modificacão à partir do momento em que o programa é rodado.

2) Você utiliza -M para verificar se arquivos de sistema sofreram vandalismo, algo estilo tripwire. Também não vai funcionar, pois se o arquivo de sistema sendo monitorado for modificado após o nosso programa monitor ser rodado o tempo entre o retornado por -M e a data de alteracão será negativo! O que não faria sentido. Seria um bug difícil de encontrar e remover.

3) Você utiliza -M para decidir se deve, ou não, efetuar backup de um ou mais arquivos. Neste caso pode ocorrer o efeito da data negativa do exemplo 2 e seus backups de dados valiosos podem não ocorrer conforme planejado.

Então, o que fazer?

Claro que em programas quebra-galho, que rodam em minutos ou segundos, a não ser que se estejam organizando longas listas de arquivos por data de modificacão, provavelmente não será preciso se preocupar com isso.

Já em programas de longa duracão será preciso ter cuidado. Como todos os problemas em Perl, há sempre mais de uma solucão.

As duas solucões que conheco utilizando -M envolvem refrescar a data inicial à partir da qual -M efetua sua comparacão. E qual é essa data? É aquela gravada na variável especial $^T.

A terceira solucão que encontrei é deixar de usar -M em favor de stat().

Confira o seguinte programa:


#!/usr/bin/perl

print "$^T\n";
sleep(30);
print "$^T\n";

Rodando, temos o seguinte resultado:


root@clapton:~/perl# perl modificado2.pl
1194827682
1194827682

Aí está o nosso culpado. -M utiliza $^T para determinar a data de modificacão dos arquivos mas $^T não muda durante o runtime de nosso programa. Assim, tenho em mente as seguintes solucões, crie a sua ou escolha a mais apropriada.

Solucão 1 – refrescar $^T toda vez que usar -M.
Exemplo:

#!/usr/bin/perl

$data = -M "/etc/passwd";
print "$data\n";

#..... PROCESSO DE LONGA DURACÃO .....

# mais tarde...
$^T = time();
$data = -M "/etc/passwd";
print "$data\n";

Solucão 2 – criar um timer com signal handler que refresca $^T para voce periodicamente. Assim há menos risco de você esquecer de refrescar $^T antes de uma chamada.

Exemplo:

#!/usr/bin/perl

# arma o relogio do SIGALARM
$SIG{ALRM} = sub { $^T = time(); alarm 5; };

# dispara a 1a vez, daqui a 5 segundos
alarm 5;

print "$^T\n";

#..... PROCESSO DE LONGA DURACÃO .....

# $^T deve estar atualizado, com um erro máximo de 5 segundos
# ajuste o valor do tempo do alarm de acordo com a necessidade
print "$^T\n";

Rodando o código acima, substituindo #….. PROCESSO DE LONGA DURACÃO ….. por um sleep(30), obtemos:


root@clapton:~/perl# perl modificado_alarm.pl
1194828811
1194828816

Percebeu que o tempo inicial não é o tempo final menos 30 segundos? Lembre-se que o sleep() acima é apenas para ilustrar um processo demorado. O sinal ALRM interrompe o sleep() aos 5 segundos(tempo que escolhemos para o alarm()) e retorna na linha após ele. Tratar corretamente de sinais UNIX é assunto para outro artigo. Lembre-se que o sinal interrompe tudo que está acontecendo e retorna no procedimento seguinte ao que foi interrompido. Saiba mais clicando aqui(link em inglês, se você conhecer em português por favor indique nos comentários deste post, obrigado).

Solucão 3 – use stat() no lugar de -M

stat() sempre retorna as datas de modificacão em segundos, assim basta subtrair de time() e teremos sempre preciso o periodo desde que o arquivo foi modificado.

Exemplo:

#!/usr/bin/perl

($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("/etc/passwd");

$modificado_segundos = time() - $mtime;
print "$modificado_segundos\n";

#..... PROCESSO DE LONGA DURACÃO .....
sleep(30);

$^T = time();
($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("/etc/passwd");

$modificado_segundos = time() - $mtime;
print "$modificado_segundos\n";

Obtemos:


root@clapton:~/perl# perl modificado_stat.pl
44439213
44439243


Lembre-se que, utilizando stat(), você obtém o retorno em segundos. Se o seu programa utilizar o número de dias e fracão de dia retornada pelo -M você deve se lembrar de converter esse dado dividindo o valor retornado do time() – stat()[9] por 86400(número de segundos num dia).

Como em todos os artigos aqui do blog, se tiver sugestões e outras solucões para este problema, envie através do campo de comentários aqui do blog. As melhores sugestões serão publicadas juntamente com o devido crédito ao autor.

O que você acha?