quarta-feira, 31 de agosto de 2016

Cobertura dos n primeiros primos

Uma questão bastante simples serviu para eu revisar e combinar duas ferramentas matemáticas: o princípio da inclusão-exclusão e as combinações.

A dúvida era: dados n primeiros primos, qual porcentagem dos inteiros é de múltiplos de ao menos um deles.

Se n=1, o único primo a ser considerado é o 2. Obviamente, metade dos inteiros é múltiplo de 2.

Com n=2, temos os primos 2 e 3. É preciso somar 1/2 e 1/3, mas também subtrair 1/6, porque esses 1/6 já foram contabilizados num caso ou noutro. Aí já vemos que precisamos das combinações e logo depois do princípio da inclusão-exclusão: somam-se as porcentagens de cada primo, depois subtraem-se as combinações dois a dois.

Então, preciso de duas funções e uma lista de primos. A função de combinações (choose) está descrita em detalhes noutro artigo.


#!/usr/bin/perl

my @primes=qw(2 3 5 7 11 13 17 19 23 29);

sub choose {
  my $n=shift;
  my $k=shift;
  my $callback=shift;
  my @rest=@_;
  
  if($k>0) {
    for my $m ($k..$n) {
      choose($m-1, $k-1, $callback, $m, @rest); 
      &$callback($m, @rest) if $k==1;
    }
  } 
}

for $n (1..50) {
  my @g=@primes[0..$n-1];
  my $sum=0;
  my $op=1;
  for my $k (1..$n) {
    my $partial=1;
    choose($n, $k, sub {
      my $product=1;
      map { $product*=$g[$_-1] } @_;
      $sum+=($op*(1/$product));
    });
    $op*=-1;
  }    
  print "$sum\n";
}

Esse código imprime a porcentagem dos inteiros que são múltiplos dos n primeiros primos, até o n=50. Eu reduzi a lista dos primos para encurtar o código, mas é fácil obter a lista dos primeiros 1000 primos.

O cálculo começa a ficar lento com n=15 ou nessa redondeza, conforme o computador.

O gráfico abaixo mostra a evolução. Ela parece convergir para algo perto de 0,9, mas na verdade ela vai muito lentamente em direção ao 1.


Agora, o que me interessa são as frações que esses números representam. Os primeiros são 1/2, 2/3, 11/15, 27/35.

terça-feira, 23 de agosto de 2016

Telegram bot no OpenWRT

O roteador TP-Link WR842ND, além de ser relativamente barato, possui uma porta USB e permite rodar o OpenWRT. O desempenho dele (266,64 Bogomips) é semelhante ao de um Pentium 133. Ele possui 8MB de Flash e 32MB de RAM. Então, é uma máquina com potencial. Seria uma pena que fosse usada apenas para oferecer wifi.

Adicionei um pendrive de 16GB e um amigo gentilmente instalou e configurou o OpenWRT (Gargoyle, para ser mais preciso). Além de roteador, com muitos gigas livres, ele pode virar um servidor de arquivos e posso deixar um download grande rodando nele durante a noite, por exemplo.

Ele não oferece o X, obviamente, mas tem uma linha de comando completa via o BusyBox. Já que tenho mais memória via o pendrive, instalei o Perl para facilitar as tarefas.

Para gerenciá-lo remotamente, resolvi escrever um bot do Telegram. E para dar mais emoção, decidi usar o que estivesse já instalado. Só abri exceção para o curl, porque não sou tão masoquista. O shell disponível é o ash: limitado, mas pequeno e eficiente.

O Telegram oferece uma interface sobre HTTP com mensagens em JSON. Por sorte, o OpenWRT já oferece uma ferramenta para processar JSON: jsonfilter. Por azar, não tem nenhuma documentação. Só encontrei o modo de usar no código-fonte.

Então, a única peça que faltava era o curl.

opkg install curl


E esse comando instala um executável de apenas 69KB.

Para enviar uma mensagem com o IP do roteador, um script simples basta:

#!/bin/ash

ip=$(ifconfig eth0 | grep inet | grep -v inet6 | awk '{print $2}')

api=https://api.telegram.org/bot<key>
curl -k -s -X POST $api/sendMessage?chat_id=<id>&text=$ip


A opção -k indica que o curl pode ignorar os erros de certificado, o -s suprime as mensagens e o -X precede o POST.

Resolvi colocar cada comando em um arquivo distinto. Para retornar o resultado ao script de controle, duas variáveis são necessárias: telegram_reply para indicar se houve sucesso no processamento e telegram_message com o texto da resposta. Talvez no futuro seja preciso adicionar telegram_type, se eu quiser enviar fotos, por exemplo. O script para tratar o comando /ip fica assim:

#!/bin/ash

ip=$(ifconfig eth0 | grep inet | grep -v inet6 | awk '{print $2}')

telegram_message=$ip
telegram_reply=true

O método getUpdates (que informa os últimos comandos) pode receber um parâmetro para indicar quais comandos já foram respondidos. Para manter registro desse parâmetro, criei um arquivo /tmp/bot_vars.sh. Ele conterá apenas uma linha na forma offset=123.

Então, o script que solicita os comandos e os encaminha para os scripts específicos precisa:
  1. Solicitar os comandos;
  2. Extrair os ids das solicitações, os ids dos solicitantes, e os comandos propriamente ditos;
  3. Para cada comando, separar os parâmetros e invocar o respectivo script;
  4. Se o script retornou com sucesso, enviar a mensagem produzida;
  5. atualizar o arquivo /tmp/bot_vars.sh com o id do último comando.

#!/bin/ash

key='segredo';
api="https://api.telegram.org/bot$key"
source /tmp/bot_vars.sh
offset=$(($offset+1))
status=0
updates=$(curl -s -k -X GET $api/getUpdates?offset=$offset)
status=$(jsonfilter -s "$updates" -e $.ok)
if [ $status = 'true' ]; then
  update_ids=$(jsonfilter -s "$updates" -e $.result[*].update_id)

  for update_id in $update_ids
  do
    sender=$(jsonfilter -s "$updates" \
                        -e "$.result[@.update_id=$update_id].message.from.id")
    command=$(jsonfilter -s "$updates" \
                         -e "$.result[@.update_id=$update_id].message.text")
    cmd=$(echo $command |  awk '{print $1}')
    parms=$(echo $command | awk '{$1=""; print $0}')

    telegram_reply=false
    case $cmd in
      '/ip') source ip.sh $parms ;;
    esac

    if [ $telegram_reply = 'true' ]; then
      curl -k -s -X POST \
        $api/sendMessage -d chat_id=$sender -d text="$telegram_message"
    fi
    
  done

  if [ -n "$update_ids" ]; then
    echo "offset=$update_id" > /tmp/bot_vars.sh
  fi
fi

Não é o quadro mais lindo do museu, mas, sendo shell, não se poderia esperar muito mais. Agora, tenho que me preocupar com os problemas de segurança que isso deve ter.

quinta-feira, 9 de junho de 2016

Logins

Com quase dois milhões de logins registrados no meu log, resolvi verificar em quais horários as pessoas estão mais alertas e erram menos suas senhas.

Os resultados foram surpreendentes.

Hora Erros/Sucessos Total de tentativas
00 10,48 7270
01 36,94 7551
02 97,12 7163
03 104,19 6522
04 440,07 6175
05 73,77 6281
06 8,46 6928
07 0,52 45142
08 0,42 169947
09 0,47 209327
10 0,47 223268
11 0,47 175897
12 0,47 79421
13 0,42 163371
14 0,45 200312
15 0,44 187376
16 0,43 164907
17 0,51 75282
18 0,83 27162
19 1,61 14589
20 2,12 12932
21 2,52 12246
22 2,77 11047
23 4,24 8708

As primeiras horas da manhã não são as melhores horas para trabalhar. Eu não faria testes numa usina nuclear nestas horas. Os 440 erros para cada sucesso às 4h da manhã são deveras surpreendentes. Imagino que as pessoas tentem uma vez ou outra e simplesmente desistam, resolvendo esperar o sol aparecer.

Excluindo os IPs suspeitos (e devidamente bloqueados), a situação melhora um tanto.

Hora Erros/Sucessos Total de tentativas
00 6,11 4504
01 23,01 4779
02 58,9 4373
03 59,19 3732
04 244,64 3439
05 41,07 3534
06 4,7 4173
07 0,43 42420
08 0,4 167186
09 0,45 206585
10 0,45 220532
11 0,44 173169
12 0,42 76697
13 0,4 160585
14 0,43 197555
15 0,42 184612
16 0,4 162143
17 0,45 72495
18 0,65 24442
19 1,13 11882
20 1,46 10194
21 1,73 9517
22 1,82 8274
23 2,56 5919

As melhores horas foram 8h, quando presumo que as pessoas estejam começando a jornada, e 13h, quando presumo que estejam voltando do almoço.

Visando encontrar algum sentido nos números da madrugada, excluí todas as linhas relativas a tentativas vindas de IPs dos quais nunca houve um login com sucesso.

Hora Erros/Sucessos Total de tentativas
00 0,81 1155
01 4,54 1108
02 6,23 528
03 0,72 107
04 2,64 51
05 1,07 174
06 0,26 929
07 0,32 39324
08 0,37 164102
09 0,42 203523
10 0,42 216503
11 0,41 169326
12 0,35 73287
13 0,35 156826
14 0,39 193020
15 0,38 180017
16 0,36 157655
17 0,37 68708
18 0,41 20924
19 0,49 8317
20 0,57 6521
21 0,54 5365
22 0,49 4368
23 0,45 2426

Com isto, os números da madrugada revelam que há muitos engraçadinhos tentando entrar onde não deviam.

quarta-feira, 1 de junho de 2016

Ajustando tablespaces com facilidade

Uma tarefa recorrente na manutenção de um banco de dados Oracle é o de colocar objetos nos tablespaces corretos. Para facilitar o trabalho, recorri ao metadados. A rotina abaixo procura todas as tabelas, todos os índices e todos os LOBs de um schema e verifica se estão nos tablespaces indicados.

create or replace procedure 
  ajustar_tablespaces(p_schema varchar2, 
                      p_table_ts varchar2, 
                      p_index_ts varchar2, 
                      p_lob_ts varchar2) is
begin

  --Indices
  for r in 
   (select 'alter index '||p_schema||'.'|| index_name 
         ||' rebuild tablespace '||p_index_ts cmd 
    from all_indexes
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_index_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --Tabelas
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move tablespace '||p_table_ts cmd 
    from all_tables 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_table_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

  --LOBs
  for r in 
   (select 'alter table '||p_schema||'.'||table_name
         ||' move lob('||column_name||') store as (TABLESPACE '||p_lob_ts||')' cmd 
    from all_lobs 
    where owner=upper(p_schema) 
      and tablespace_name<>upper(p_lob_ts))
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;
  
  --Reconstroi os indices
  for r in 
   (select 'alter index '||p_schema||'.'||index_name
         ||' rebuild' cmd  
    from all_indexes 
    where owner=p_schema 
      and status='UNUSABLE') 
  loop
    dbms_output.put_line(r.cmd);
    execute immediate r.cmd;
  end loop;

end;
Então, para corrigir os tablespaces dos objetos de um schema, eu poderia fazer uma chamada como esta:

  exec ajustar_tablespaces('INFO', 'ts_data01', 'ts_index02', 'ts_lobs');

Os comandos são sendo impressos para que se possa acompanhar o andamento do trabalho, principalmente se os objetos forem grandes.

sexta-feira, 27 de maio de 2016

Flashback de código no Oracle

Depois de uma tentativas desastrada de parcialmente atualizar uma package, precisei usar o flashback para recuperar o código original. Isso é fácil no Oracle:

  select text 
  from all_source as of timestamp sysdate -1/24 --uma hora
  where owner=:owner and name=:package_name and type='PACKAGE BODY'

Infelizmente, o SQLDeveloper exporta o texto com linhas adicionais. Contornar isso com um pouco de Perl é simples:

  perl -ne "print if !/^$/" package.sql > package_sem_linhas_vazias.sql

Esta linha de Perl imprime todas as linhas do arquivo (último parâmetro) que tiverem alguma coisa (/^$/ é verdadeiro para linhas vazias).

sábado, 13 de fevereiro de 2016

Solução caseira para segurança doméstica

As soluções de segurança que tenho visto são caras, então resolvi desenvolver uma solução caseira. Decidi que com um Raspberry Pi, uma câmera barata e um pouco de código, poderia fazer algo tão bom quanto sistemas dez vezes mais caros.

O mais importante é que o sistema deveria enviar notificações para o celular. O Whatsapp não permite robôs, então recorri ao Telegram.

Desenvolvi usando um Raspberry Pi B, mas o sistema final roda num Raspberry Pi A+ (que é ainda menor e mais barato). Acredito que funcionaria igualmente bem num Raspberry Pi Zero (que custa apenas US$5).

A ideia do projeto é bastante simples: a câmera, quando ativada por movimento, copia imagens para o Raspberry Pi e este, por sua vez, as encaminha para uma conta do Telegram. O Telegram entrega as fotos para o meu celular.

Se o código da câmera fosse aberto, ou ela oferecesse alguma API, seria possível que ela falasse diretamente com o Telegram. Ponto negativo para o código fechado.

Ingredientes
  • Um Raspberry Pi B (512MB de RAM, 700MHz de CPU e custa ~US$25);
  • Uma câmera D-Link 930LB (custa menos de R$200,00);
  • Uma conta no Telegram (Whatsapp não aceita robôs);
  • Um cartão de memória SDHC de 16GB (um de 8GB já resolveria);
  • Um pouco de Perl.
Passos

1. Gravar Raspbian no cartão.

2. Instalar um servidor de ftp (sudo apt-get install vsftpd).

3. Configurar o servidor de ftp (/etc/vsftpd.conf).

É preciso habilitar a possibilidade de escrever e de fazer login com contas locais:


local_enable=YES
write_enable=YES


4. Configurar um ramdisk (para não desgastar o cartão de memória, se o número de escritas fugir ao controle e porque é mais rápido).

É preciso adicionar esta linha ao arquivo /etc/fstab:


tmpfs  /home/pi/tmp  tmpfs  nodev,nosuid,size=1M 0 0


Esse comando cria um espaço de 1MB que fica montado no diretório tmp/ dentro do home do usuário pi, mas poderia ser qualquer outro lugar. Os arquivos não passam de 50KB, então essa área poderia ser muito menor.

5. Configurar a câmera.

Configurei a câmera para fazer o ftp da imagem quando for detectado um movimento (usei sensibilidade de 30%, porque ela envia muitas imagens com mais que isso).

O nome do arquivo indiquei como DCS-930LB e isso acaba resultando num arquivo JPEG chamado DCS-930LB.jpg.

6. Criar uma conta no Telegram.

É preciso procurar a conta @BotFather e conversar. Ele dá todas as instruções.

7. Programar em Perl (essa é a melhor parte).

É preciso instalar algumas bibliotecas, o curl, o cpanm e o módulo WWW::Telegram::BotAPI:

sudo apt-get install libnet-ssleay-perl libio-socket-ssl-perl --fix-missing
sudo apt-get install curl
sudo curl -L http://cpanmin.us | perl - --sudo App::cpanminus
sudo cpanm WWW::Telegram::BotAPI

Usei um modelo de robô e fiz algumas adaptações.

Em primeiro lugar, criei ums lista das contas autorizadas (para que estranhos não brinquem com meu robô):


my %chat_ids=(
  123456789=>'forinti',
  987654321=>'derpina'
);


Depois, mudei o loop principal para descansar 5s a cada iteração, verificar se a imagem mudou (pela hora de criação), e enviar a imagem se algo novo surgiu.


my $paused=0;
my $last_update=get_last_update();
while(1) {
  sleep(5);
  my $update=get_last_update();
  if(!$paused && $update>$last_update) {
    for my $id (keys %chat_ids) {
      $api->sendPhoto({
         chat_id=>$id, 
         photo=>{file=>'/home/pi/tmp/DCS-930LB.jpg'}
      });
    } 
    $last_update=$update;
  }
  ...


Finalmente, criei comandos para recuperar a última imagem (/li), parar o envio (/pause) e reiniciar o envio (/go).

sub get_last_update {
  return (stat('/home/pi/tmp/DCS-930LB.jpg'))[9];
}

sub get_last_image {
  return {
    method=>'sendPhoto',
    photo=>{ file=> '/home/pi/tmp/DCS-930LB.jpg' }
  };
}

sub pause {
  $paused=1;
  return 'Parou. Por enquanto...';
}

sub go {
  $paused=0;
  return 'Reiniciou.';
}


Resultado

Consegui produzir um sistema de segurança bastante simples e barato. Ele consome pouquíssima energia (menos de R$10,00 para operar um ano inteiro) e não precisa ser monitorado já que envia alertas. Por usar pouca energia, poderia funcionar ligado a uma bateria ou a um painel solar. Se conectasse a uma rede de celular, seria realmente resiliente.

terça-feira, 19 de janeiro de 2016

A linha mais comprida de um arquivo

Eu precisava descobrir quantas colunas tinha a linha mais comprida de um arquivo. O único porém é que o arquivo é muito grande.

Primeiro, tentei um pouco de shell.

while read line; do echo -n "$line" | wc -c; done< arquivao  | sort | uniq -c

Isso deveria apresentar o número de linhas com determinado comprimento e o número de ocorrências de cada comprimento (não há muita variação nos comprimentos das linhas, embora haja muitas linhas).

Infelizmente, não tive paciência de deixar esta pequena solução terminar. Resolvi averiguar se o Perl seria mais rápido.

perl -MData::Dumper -n -e '$n{length($_)}++; END {print Dumper(%n)}' arquivao


A solução é bem simples e muito mais rápida que a primeira. Com os parâmeteros -n e -e, o bloco de código é aplicado a cada linha do arquivo (último parâmetro). Cada tamanho de linha vira uma chave no hash %n e é incrementada a cada ocorrência. No fim, uso o módulo Data::Dumper para apresentar o que há dentro de %n.