Resolvendo o limite da função Randomize()

Introdução

No post Boas Práticas de Programação – Código Simples, Resultados Rápidos, eu mencionei um limite operacional da função Randomize() do AdvPL. Caso a diferença entre o maior e o menor número a ser sorteado for maior que 32767, a função vai sortear um número maior ou igual ao número inicial informado, e menor que o número inicial mais 32767.

Programa original

A chamada no fonte original era para sortear um número entre 100000 e 999999. Porém, devido a limitação operacional da função Randomize(), o maior resultado seria sempre menor que 132767.

nTmp := Randomize(100000,999999)

Para resolver esta limitação da função, podemos criar uma segunda função de sorteio, onde podemos tratar este limite de uma forma elegante e performática. Vamos criar a USER Function Randomic(). 

USER Function Randomic(nMin,nMax)
Local nDiff := nMax-nMin
Local nDec := 0
If nDiff < 32766
   Return Randomize(nMin,nMax)
Endif
While nDiff > 32765
   nDiff /= 10 
   nDec++
Enddo
nTmp := randomize(0,int(nDiff))
While nDec > 0 
   nTmp *= 10 
   nTmp += randomize(0,10) 
   nDec--
Enddo
Return nMin+nTmp

Como a U_Randomic() funciona

Inicialmente, determinamos usando a variável local nDiff qual é a diferença do maior para o menor número. Caso a diferença seja suportada pela função Randomize(), retornamos direto a chamada para a função Randomize(), passando os parâmetros originais.

Caso a diferença não seja contemplada pela função Randomize(), dividimos a diferença por 10 até que ela entre dentro do intervalo. Cada divisão realizada incrementa uma unidade na variável local nDec, que indica quantas casas da diferença original foram reduzidas. Uma vez que a diferença se enquadre nos valores suportados, chamamos a função Randomize(), para sortear um número entre 0 e o valor inteiro da diferença apurada após as divisões.

Sorteado este número, agora precisamos sortear mais alguns números para completar as casas decimais que foram “cortadas” da diferença original. A cada iteração, para quantas casas decimais foram cortadas — valor guardado na variável nDec — o valor anteriormente sorteado é multiplicado por 10, e um novo valor sorteado entre 0 e 9 é adicionado ao resultado, decrementando uma unidade em nDec. Terminado o processo, o número final sorteado será o número inicial somando com o número  final sorteado, armazenado em nTmp.

Exemplo

Vamos rodar o programa de testes abaixo, chamando uma vez a função U_RANDOMIC() usando o IDE/TDS em modo de depuração.

User Function TstRand()
Local nRand
nRand := U_Randomic(100000,1000000)
conout(nRand)
Return

Ao entrarmos na função U_Randomic(), a diferença entre o numero final e o inicial será de 900000 (novecentos mil). Ao passar pelo primeiro loop, a primeira divisão por 10 faz o número baixar para 90000 (noventa mil), e nDec é incrementado para uma unidade. Como o número ainda é maior que 32765, este loop é executado novamente, onde nDiff baixa agora para 9000 (nove mil) e nDec é incrementado para 2. Como agora nDiff é menor que 32765, o programa continua.

O sorteio do novo número, a ser armazenado na variável nTmp, será feito usando a função Randomize(), informando o valor mínimo 0 e o máximo 9000. Durante a depuração, o número sorteado por exemplo foi 1271.

Agora, como houve a redução de duas casas decimais, o próximo loop será executado duas vezes. Na primeira execução, o número nTmp é multiplicado por 10 — resultando em 12710 — e um novo dígito entre 0 e 9 será sorteado e acrescentado em nTmp. foi sorteado o número 7, e acrescentado em nTmp, fazendo seu valor atual ser 12717. Na segunda execução, este valor foi multiplicado novamente por 10 — resultando em 127170 — e um novo número foi sorteado e acrescentado — foi sorteado o número 4, e nTmp passou a conter o valor 127174.  Nas duas execuções, nDec foi decrementado duas vezes, voltando a ser 0 (zero). Pronto, na ultima linha, acrescentamos o número inicial (100000) ao número sorteado (127174), resultando no número 227174.

Da forma que a função foi escrita, ela torna muito rápido e seguro o processo de sorteio, não havendo chance do valor sorteado ser maior que o especificado como parâmetro para a função.

Operadores especiais utilizados

Quando queremos realizar uma operação aritmética com uma variável, e queremos atualizar esta mesma variável com o valor resultante da operação, normalmente utilizamos uma sintaxe como:

variavel := variavel <operador> <operando>

Por exemplo:

nVar := nVar * 10 
nVar := nVar + 10 
nVar := nVar + 1

Em AdvPL, existem operadores compostos  especiais, que ao mesmo tempo fazem a atribuição e a operação aritmética. Por exemplo, as operações acima podem ser escritas da seguinte forma:

nVar *= 10
nVar += 10 
nVar++

Os operadores +=-=*=  e  /= são binários — exigem dois argumentos, a variável do lado esquerdo que será usada como base para a operação e receberá o resultado, e a expressão do lado direito (variável ou constante), que será utilizada para realizar a operação. Respectivamente são os operadores de soma (+), subtração (-), multiplicação (*) e divisão (/). Já os operadores ++ e — são unários — têm apenas um argumento, que é a variável em questão, e respectivamente somam ou subtraem o valor 1 da variável informada.

Conclusão

Mesmo que alguma função básica da linguagem AdvPL possua alguma restrição operacional, o conhecimento das demais funcionalidades e capacidades da linguagem podem tornar fácil uma implementação de um novo recurso que atenda a sua necessidade.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 😀

Referências

 

 

Manipulação de arquivos em AdvPL – Parte 01

Introdução

Faz algum tempo, eu publiquei um post sobre desempenho em AdvPL, criando uma classe em AdvPL para leitura de arquivos de texto simples (Acelerando o AdvPL – Lendo arquivos TXT). Porém, o exemplo em si já parte da premissa que o programador conhece o que e como funcionam as funções de manipulação de arquivos funcionam. Neste post, vamos ver o que elas fazem “por dentro”, para entendermos como podemos utilizá-las.

Um arquivo no disco

Qualquer arquivo gravado em uma unidade de disco ou em um dispositivo com sistema de arquivos (Drive USB, CD-ROM,etc…) não passa de uma sequência finita de bytes, que representam um conteúdo. Um arquivo (no Windows) normalmente possui um nome e uma extensão, separados pelo caractere “.” (ponto), onde a extensão indica ao sistema operacional qual é o tipo do conteúdo deste arquivo. O arquivo possui um tamanho alocado na unidade na qual ele está gravado — quantos bytes têm o arquivo — além da data e hora da última gravação feita no arquivo, e ainda pode conter atributos especiais.

Uma imagem gravada em disco possui uma representação digital da imagem real, em uma determinada resolução (ou grid de pontos em linhas x colunas), onde o formato utilizado indica ao programa como as sequencias de bytes devem ser tratadas para representar a imagem que o arquivo contém.

Arquivos TEXTO

Um arquivo de texto simples — por exemplo com a extensão “.txt”, nada mais é do que a representação em bytes de todas as letras, números, espaços, pontuação, letras acentuadas e quebras de linha. Normalmente um arquivo texto simples utiliza a representação da tabela ASCII — onde uma faixa de bytes com os valores de 0 a 31 representam caracteres de controle ( quebra de linha, tabulação e outros), do 32 as 127 representam letras maiúsculas e minúsculas, números — representação do número como texto –, espaço em branco, caracteres e símbolos gráficos comuns (exclamação, arroba, dólar, percentual, asterisco, hífen, colchetes, aspas simples e duplas, parênteses, e afins; e por fim a faixa de 128 a 255 é utilizada para representar letras acentuadas, caracteres gráficos e especiais. Esta representação é conhecida como TABELA ASCII.

Existe uma outra padronização mundialmente conhecida de conversão de valores de bytes para caracteres. Chama-se EBCDIC — vide mais detalhes nas referências no final do post — criada pela IBM na década de 60, praticamente junto da padronização do ASCII. Esta tabela foi usada nos equipamentos IBM a partir do Mainframe System/360, usada ainda hoje em Mainframes e Mid-Range Computers da IBM.

Outros formatos de arquivo

Existem vários outros formatos de arquivo, cada formato na verdade é uma especificação de como o seu conteúdo deve ser tratado. A Tabela ASCII é convenção de representação de códigos numéricos para caracteres, criada para padronizar a troca de informações entre computadores. Na prática, dentro do arquivo, eu tenho uma sequência de bytes com valores de 0 a 255.

Texto e Bytes

Vamos partir de alguns exemplos, para deixar mais claro o que tem dentro dos arquivos, e das variáveis do tipo “C” Caractere do AdvPL, usando algumas funções. Por exemplo, se eu quero criar em AdvPL uma variável caractere com as letras A, B e C (maiúsculas, sem acento), eu posso simplesmente fazer uma atribuição direta a uma variável, usando um conteúdo literal.

cTexto := "ABC"

Na área de memória usada para armazenar estes três caracteres, é usado um byte para cada caractere. O valor guardado dentro do Byte é o valor numérico da representação da letra na TABELA ASCII. Por exemplo, a letra “A” é o número 65“B” é 66 e “C” é 67.

Eu poderia escrever isso de outra forma. Por exemplo, usando a função CHR(), que recebe o número do byte e retorna a representação em string do mesmo. Por exemplo:

cTexto := chr(65) + chr(66) + chr(67)

O conteúdo da variável cTexto será exatamente igual nas duas formas de atribuição. Um arquivo texto pode ter algumas diferenças entre sistemas operacionais. Por exemplo, no Windows uma quebra de linha do arquivo é representada pela sequência de bytes chr(13)+chr(10). Já em Linux, é usado apenas chr(10). Logo, eu posso montar a representação de uma linha de um arquivo texto na memória usando:

cLinha := "Uma linha de texto." + chr(13) + chr(10)

Funções de baixo nível de manipulação de arquivo

Existem algumas funções da linguagem AdvPL, projetadas para ler e gravar bytes de qualquer arquivo. Porém, nenhuma destas funções fazem qualquer crítica ao tipo de conteúdo do arquivo — isto é, não importa se o arquivo tem uma imagem, um texto ou um vídeo: Para estas funções, o arquivo é simplesmente uma sequência de bytes. São elas:

  • FCreate – Cria um arquivo.
  • FOpen – Abre um arquivo.
  • FSeek – Reposiciona o ponteiro de dados do arquivo.
  • FRead – Lê bytes do arquivo.
  • FWrite – Grava bytes no arquivo.
  • FClose – Fecha o arquivo
  • FError – Retorna o status de execução da última instrução de arquivo

As funções FCreate() e FOpen() retornam um número identificador, chamado de “Handler de Arquivo”, e todas as demais funções, enquanto o handler não foi fechado pela função FCLose(), recebem este identificador como parâmetro para realizar as operações descritas.

Ponteiro de Dados do Arquivo

O ponteiro de dados é uma espécie de marcador que aponta para um determinado byte do conteúdo arquivo, em uma determinada posição — que também pode ser chamado de “Offset“. O Offset inicia em 0 (zero), correspondendo ao primeiro byte do arquivo. Em um arquivo de tamanho N bytes, o ponteiro de dados pode apontar para uma determinada posição dentro do arquivo, de 0 a N-1, ou pode apontar para o final do arquivo (EOF), correspondendo a posição N.

Qualquer operação de leitura ou gravação sempre é iniciada na posição onde o ponteiro de dados está apontando, e a operação é sempre feito da posição atual em direção ao final do arquivo. Imagine um arquivo no disco como sendo uma sequência de bytes, iniciando da esquerda para a direita.

Função FCreate()

Primeiro vamos criar um arquivo. Usamos para isso a função FCreate(), onde informamos no primeiro parâmetro o caminho e nome do arquivo a ser criado. Em caso de sucesso, a função FCreate() retorna um identificador numérico, chamado de “Handler” do arquivo. Agora, vamos ver alguns detalhes sobre os comportamentos da função FCreate().

  • Caso o arquivo informado como parâmetro contenha uma unidade de disco (drive), o Application Server entende que a criação da tabela será feita pelo cliente AdvPL — SmartClient.
  • Caso o arquivo não contenha o drive ou unidade de disco, o arquivo será criado na máquina onde o Application Server está sendo executado, em uma pasta ou sub-pasta a partir do RootPath (pasta raiz de arquivos do ambiente).
  • Caso o arquivo a ser criado já exista no disco, e não esteja sendo aberto e/ou usado por outro processo, o conteúdo anterior do arquivo é perdido — ele torna-se um arquivo vazio, com 0 (zero) bytes.
  • Caso o arquivo a ser criado não exista, ele é criado vazio — com 0 byte(s).
  • Uma vez que o arquivo tenha sido criado (ou recriado) com sucesso, ele será mantido aberto em modo exclusivo, para leitura e escrita. Isto é, enquanto este programa não fechar o arquivo usando a função FClose(), o arquivo permanece aberto para o processo atual ler e gravar bytes no arquivo, e nenhum outro processo vai conseguir abrir este arquivo, mesmo que seja apenas para leitura.
  • Em caso de falha na criação (ou recriação) do arquivo, o handler retornado é o valor -1 (menos um) , indicando erro. Podemos recuperar a causa do erro chamando a função FError() após executar o FCreate(). A função FError() retorna o código do erro de criação/recriação da tabela, e retorna 0 (zero) em caso de sucesso.
User Function TSTFILE()
Local nHnd 
Local cFile := '\temp\teste.txt'
Local cLine
nHnd := FCreate(cFile)
If nHnd == -1
  MsgStop("Falha ao criar arquivo ["+cFile+"]","FERROR "+cValToChar(fError()))
  Return
Endif

Com o fonte acima, criamos um arquivo vazio, na pasta “\temp\”, que deve ser criada a partir do RootPath do ambiente atual, Esta configuração está presente nos ambientes do Protheus Server, na chave “RootPath” do ambiente.

Como  o arquivo está vazio, o ponteiro de dados do arquivo neste momento não aponta para lugar algum . Agora, vamos gravar uma linha de texto em formato Windows dentro desse arquivo, com quebra de linha.

cLine := "Olá sistema de arquivos" + CRLF

Para quem não conhece, CRLF escrito desta forma no AdvPL — junto e com letras maiúsculas — é um #translate, que retorna os dois bytes ( chr(13) e chr(10) que indicam em um arquivo texto do Windows que deve ser realizada uma quebra de linha. Este recurso está disponível no AdvPL desde que seja utilizado o #include “protheus.ch” — ou o seu equivalente, “totvs.ch”.

Agora, vamos gravar esta informação três vezes no arquivo, em três linhas, uma embaixo da outra, e em seguida fechar o arquivo

FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FWrite(nHnd,cLine)
FClose(nHnd)

Uma operação de escrita no arquivo é feita sempre a partir de onde está apontando o ponteiro de dados. Como o arquivo não tem nada após ser criado, o ponteiro de dados está em EOF (Final de Arquivo), a operação de escrita no arquivo insere os bytes/caracteres informados no final do arquivo, aumentando o seu tamanho. Após gravar os caracteres, o ponteiro de dados aponta novamente para EOF,

Feito isto, vamos ver exatamente o que foi gravado. Cada linha contém 23 caracteres (ou bytes), mais dois caracteres de controle ( chr(13)  e chr(10) ). Logo, cada linha possui na verdade 25 bytes de informação. Após gravar as três linhas e as respectivas quebras, o arquivo resultante deve ter exatamente 75 byte(s).

Função FOPEN

A função fOpen() é usada para abrir um arquivo. Podemos especificar parâmetros opcionais para definir se o arquivo será aberto para leitura, escrita, ou ambos (leitura e escrita), e ainda é possível especificar o modo de compartilhamento do arquivo — exclusivo, compartilhado, bloqueio de leitura ou bloqueio de escrita. Por default, o arquivo é aberto para leitura exclusiva — nenhum outro processo pode abrir o arquivo para fazer nada, e o processo atual pode apenas ler dados do arquivo.

Seu tratamento de erro é idêntico ao FCreate() — em caso de retorno -1 (menos um), a função FError() contém um código com mais detalhes da falha encontrada.

Veja os detalhes da documentação da função FOpen na TDN. O modo de abertura e compartilhamento da tabela é definido pela soma de dois números das listas de modo de acesso e modo de compartilhamento — apenas um elemento de cada lista. Para usarmos as constantes — que são mais “elegantes” para ler o fonte, devemos fazer um #include “fileio.ch” no início do nosso fonte AdvPL. No nosso exemplo, vamos abrir a tabela no modo default.

nHnd := FOpen(cFile)
If nHnd == -1
   MsgStop("Falha ao abrir ["+cFile+"]","Ferror " + cValToChar(fError()) )
Endif

Caso eu queira, por exemplo, abrir o arquivo em modo compartilhado de leitura e escrita, podemos usar os seguintes parâmetros:

nHnd := FOpen(cFile,FO_READWRITE + FO_SHARED)

Vale lembrar que, ao usar um modo compartilhado que permita escrita no arquivo, dois processos podem tentar escrever na mesma área de dados do arquivo. Se isto acontecer, a última gravação é a que vale. Cabe a aplicação gerenciar os acessos concorrentes ao mesmo Offset do arquivo.

Função FSEEK

Usamos a função FSeek() para mover o ponteiro de Offset de dados do arquivo. Esta função não mexe no arquivo, apenas movimenta o ponteiro de dados do arquivo o parâmetro nOffset informado, e a partir de que ponto do arquivo — ou origem — este Offset deve ser considerado. Veja a documentação oficial completa da FSeek no TDN.

Para entender melhor seu funcionamento, vamos das um exemplo de cada movimentação a partir de uma origem distinta. A origem padrão / default é 0 (zero), indicando que o nOffSet informado é considerado a partir do início do arquivo — posição 0 (zero). Veja abaixo os parâmetros a serem informados para a função.

FSeek( < nHandle >, < nOffSet >, [ nOrigem ] )

O parâmetro nOrigem é opcional. Ele indica a partir de que ponto do arquivo o parâmetro nOffset deve ser considerado. Seu valor default é 0 (zero). Ele pode ser:

  • 0 ( zero) — DEFAULT — Movimenta o ponteiro de dados para a posição informada em nOffSet, a partir do início do arquivo.
  • 1 (um) – Considera o nOffSet como sendo o número de bytes para movimenta o ponteiro de dados a partir de sua posição atual. Neste caso, nOffSet pode ser positivo ( mover o ponteiro para frente) ou negativo (mover o ponteiro para traz).
  • 2 (dois) – Movimenta o ponteiro de dados considerando nOffSet como sendo a quantidade de bytes para deslocar o ponteiro de dados a partir do final do arquivo. Neste caso, nOffSet deve ser 0 (zero) ou um valor negativo.

O retorno da função, independente dos parâmetros utilizados, será a nova posição (OffSet) do arquivo, a partir do início do arquivo. Vamos a alguns exemplos

FSeek( nHnd , 0  [ ,0 ] ) – Posiciona no primeiro byte do arquivo. Retorna o OffSet após o movimento  (zero).

FSeek( nHnd , 3 [ ,0 ]) – Posicional no quarto byte do arquivo. Retorna o OffSet após o movimento (três).

FSeek( nHnd , 0 , 2 ) – Posiciona o ponteiro de dados no final do arquivo (EOF). Neste caso o offset atual corresponde ao tamanho do arquivo — pois para um arquivo de N bytes, o final de arquivo (EOF) é justamente a posição N.

FSeek( nHnd , 5 , 1 ) – Avança o ponteiro de dados 5 bytes para frente da posição atual.

FSeek( nHnd , -5 , 1 )  – Volta o ponteiro de dados 5 bytes para traz da posição atual.

Quando utilizamos o #include “fileio.ch”, ao invés de usar os números de 0 a 2 como terceiro parâmetro da função fSeek(), podemos usar as constantes abaixo:

Número 0       Constante FS_SET         
Número 1       Constante FS_RELATIVE    
Número 2       Constante FS_END

Continuando o programa de exemplo, vamos determinar o tamanho do arquivo e mover o ponteiro de dados para a primeira posição (Offset 0) do arquivo novamente.

nTamFile := FSeek(nHnd,0,FS_END)
FSeek(nHnd,0,FS_SET)

Função FRead()

Agora, vamos ler os primeiros 25 bytes do arquivo. Para isso, usamos a função FRead(). A Documentação oficial está no TDN – FRead. Veja abaixo os parâmetros da função:

FRead( < nHandle >, < cBufferVar >, < nQtdBytes > )

A função recebe em nHandle o identificador do arquivo obtido através da função FCreate() ou FOpen(), uma variável string onde os bytes lidos do arquivo a partir da posição atual do ponteiro de dados, e a quantidade de bytes que devem ser lidas.

Por exemplo, logo após determinar o tamanho do arquivo, vamos ler os primeiros 25 bytes do arquivo:

cBuffer := ""
nRead := FRead( nHnd, @cBuffer, 25 )
conout("Bytes Lidos ... "+cValToChar(nRead))

Primeiro inicializamos uma variável do tipo “C” Caractere no AdvPL com uma string vazia, depois a passamos por referência na função FRead(), seguido do número de bytes que eu quero ler a partir da posição atual do ponteiro de dados do arquivo. A função retorna o número de bytes que efetivamente foram lidos. Caso a leitura termine devido ao final do arquivo ter sido atingido, ou caso tenha havido algum erro, o valor de retorno de FRead() pode ser 0 ou um valor menor que o tamanho que foi solicitado para ler. Em caso de erro na leitura, a função FError() deve conter maiores informações. Se ferror() retornar zero, e o conteúdo lido for menor que o solicitado, então o arquivo efetivamente chegou ao final.

Como eu gravei três linhas de 25 bytes no arquivo, inserindo as quebras de linha, eu seu que ao carregar os primeiros 25 bytes ao arquivo, eu li a primeira linha de dados do arquivo inclusive com a quebra de linha. E, se eu fizer mais duas leituras da mesma forma, eu vou ler fatalmente as linhas 2 e 3 inteiras, respectivamente.

Função FWrite()

Caso eu queira escrever alguma informação no arquivo, eu posso utilizar a função FWrite(), desde que eu tenha aberto o arquivo para escrita. A função FWrite() recebe o identificador ou Handler do arquivo como parâmetro, seguido de uma variável “C” Caractere do AdvPL, contendo uma sequência de caracteres / bytes a serem gravados no arquivo, e pode ainda receber um último parâmetro opcional, permitindo informar quantos bytes do buffer de caracteres informado deve ser salvo. Caso este último parâmetro não seja informado, a string inteira é gravada no arquivo.

De forma similar a FRead(), que começa a leitura a partir da posição atual do ponteiro de dados do arquivo, a função FWrite() começa a gravar os dados a partir da posição atual do ponteiro de dados, em direção ao final do arquivo. Caso o ponteiro de dados esteja situado em um ponto do arquivo com informações já gravadas, estas informações serão perdidas, pois deste ponto em diante a nova string informada como parâmetro será salva. Caso o ponteiro de dados atinga EOF, ou já esteja em EOF, os dados são acrescentados a partir de então, sempre no final do arquivo, aumentando o tamanho do arquivo.

Não há função ou API direta que permita inserir novos dados no meio do arquivo deslocando os demais dados para a frente. E não há função ou API direta que diminua o tamanho de um arquivo. Vamos ao exemplo, continuando o fonte anterior:

nGravado := fWrite( nHnd , replicate('-',23) )

Após ler a primeira linha do arquivo, o ponteiro de dados estará apontando para o primeiro caractere da segunda linha, que é igual a primeira? 23 bytes de dados e os dos bytes finais de quebra de linha. Da forma feita acima, eu literalmente passo por cima da linha escrevendo vários “hifens” sobre a segunda linha, sem invadir o espaço ocupado pela quebra de linha. Se formos ver o arquivo após todas estas execuções, seu conteúdo deve ser:

Olá sistema de arquivos
-----------------------
Olá sistema de arquivos

Se eu escrever por exemplo mais 4 hífens, eu passaria por cima da quebra de linha, e por cima das duas primeiras letras da terceira linha. Sem a quebra de linha, o arquivo ficaria apenas com duas linhas, com o seguinte conteúdo:

Olá sistema de arquivos
---------------------------á sistema de arquivos

Observações finais

  • Se eu abrir um arquivo no modo de escrita, mesmo assim eu ainda consigo ler dados do arquivo.
  • Se eu abrir um arquivo apenas para leitura, a função FWrite() não vão conseguir gravar nada, vai retornar 0 (zero), e a função FError() retornará o código de erro correspondente.
  • Ao usar a função FOpen(), procure especificar o modo de abertura e o modo de acesso, sempre.

Conclusão

Por incrível que pareça, esta é apenas a primeira parte sobre as funções de baixo nível de arquivos em AdvPL. Ainda têm mais coisas para ver no próximo post desta sequência, porém com essa introdução já dá para se ter uma ideia do que é possível fazer em AdvPL.

Agradeço novamente a audiência, as curtidas, compartilhamentos, dúvidas e afins. Desejo a todos TERABYTES DE SUCESSO !!! 

Referências

 

CRUD em AdvPL ASP – Parte 01

Introdução

Nos posts anteriores sobre o CRUD em AdvPL, o programa de exemplo partiu de uma agenda de contatos, escrita originalmente para ser executado via SmartClient. Agora, vamos aproveitar algumas partes do “núcleo” do programa agenda.prw, e criar uma interface de manutenção para WEB, usando AdvPL ASP. Eu recomendo fortemente que você, caso ainda não tenha lido, leia os posts sobre AdvPL ASP e CRUD, eles estão acessíveis através das respectivas categorias no menu inicial superior do BLOG, e também nas referências no final deste post.

Agenda em AdvPL ASP

Inicialmente, vamos aproveitar o programa aspthreads.prw, que serve de casca para execução das nossas aplicações em AdvPL ASP, para desviar a execução do código para um novo fonte, chamado wagenda.prw, quando a URL informada for http://localhost/agenda.apw

Para isso, de posse do fonte aspthreads.prw, dentro da função U_ASPConn(), criamos uma nova entrada no DO CASE para a agenda, acrescentando as linhas abaixo:

  case cAspPage == 'agenda'
    // Executa a agenda em AdvPL ASP 
    // Os controles e páginas estão encapsulados pela função U_WAgenda()
    cReturn := U_WAGENDA()

Agora, vamos criar o fonte wagenda.prw, por partes, para ele funcionar como uma máquina de estado de tela única, de forma similar ao que fizemos com o programa no SmartClient. Algumas funções do fonte agenda.prw deverão ser alteradas, para serem visíveis para este novo fonte. Inicialmente, precisamos de uma tela APH para desenhar a interface da agenda. Vamos começar o fonte wagenda.prw de forma simples, e ir incrementando ele aos poucos.

#include "protheus.ch"

User Function WAgenda()
Local cHtml := ''
If empty(HTTPSESSION->LOGIN)
  // Usuário ainda não está logado. 
  // Retorna para ele a tela de login
  cHtml := H_WLogin()
Else
  cHtml := H_WAgenda()
Endif
Return cHtml

Aqui fazemos uso de uma variável de SESSION que o programa mesmo vai criar, para exigir que apenas um usuário autenticado — que passou primeiro pela página de LOGIN — tenha acesso à agenda. Para maiores detalhes sobre o funcionamento das SESSIONS no AdvPL ASP, consulte o post Protheus e AdvPL ASP – Parte 03.

Caso o usuário abra diretamente a página da agenda (http://localhost/agenda,apw), na verdade ele var receber uma tela HTML com um formulário de Login. Vamos ver como esta tela seria — arquivo wLogin.aph

*** OBSERVAÇÃO : Por hora os arquivos APH dos exemplos abaixo estão todos usando o CODEPAGE ANSI (ou CP1252) , e foram criadas manualmente usando o IDE do Protheus. Estas páginas também utilizam o padrão HTML5. PAra pbter mais detalhes sobre como o Web Browse trata as versões de HTML e JavaScript, uma excelente fonte de informações é o site W3SCHOOLS

<% 
/* ----------------------------------------------------------------
Login da Agenda
---------------------------------------------------------------- */ 
%> 
<!DOCTYPE html>
<html>
<head>
<meta charset="ANSI">
<title>LOGIN</title>
<style>
html, body { height: 100%; } 
html { display: table; margin: auto; }
body { display: table-cell; vertical-align: middle; }
.agbutton {
display: inline-block;
text-decoration : none;
width: 120px;
height: 18px;
background: rgb(240, 240, 240);
text-align: center;
color: black;
padding-top: 4px;
}
.agget { 
display: block;
width: 110px;
height: 22px; 
color: black; 
padding-top: 6px; 
text-align: right;
}
.aginput { 
width: 320px;
height: 22px; 
color: black; 
padding-top: 0px; 
padding-right: 10px; 
text-align: left;
}
</style>

function doLogin() { document.getElementById("F_LOGIN").submit(); };

</head>
<body style="font-family:Courier New;font-size: 12px;background-color:rgb(128,128,128);">

<form id="F_LOGIN" action="/login.apw" method="post">
<table style="border-spacing: 1px; background: rgb(192,192,192);">
<tr><td colspan="2" style="width: 500px; height: 22px; color: white; padding-top: 4px; background: rgb(128,0,0);">
<center>LOGIN</center></td></tr>
<tr><td class="agget">Usuário</td> <td class="aginput"><input id="I_USER" type="text" name="USER" size="50" ></td></tr>
<tr><td class="agget">Senha</td> <td class="aginput"><input id="I_PASS" type="password" name="PASS" size="32" ></td></tr>
<% If !empty(HTTPPOST->_ERRORMSG) %>
<tr><td colspan="2" style="width: 500px; height: 22px; color: white; padding-top: 4px; background: rgb(128,0,0);">
<center><%=HTTPPOST->_ERRORMSG%></center>
</td></tr>
<% Endif %>
<tr>
<td class="agget">&nbsp;</td>
<td>
<a class="agbutton" id="btnConfirm" href="javascript:void(0)" 
  onclick="doLogin()">Confirmar</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a class="agbutton" id="btnVoltar" href="/">Voltar</a>
</td>
</tr>
</table>
</form>
</body>
</html>

Somente nesta tela nós temos a utilização de vários recursos do Web Browser, inclusive JavaScript, e recursos do AdvPL ASP. Vamos ver um por um, e por quê foi optado por ser feito desta forma.

Formulário HTML

Inicialmente, para ser possível o Web Browse realizar um POST informando campos e valores informados pelo usuário para o Web Server, precisamos criar um formulário HTML usando a tag form, e dentro do corpo do formulário colocar tags de input para permitir o usuário informar valores para os campos determinados.

<form id="F_LOGIN" action="/login.apw" method="post">

Damos a este formulário um identificador (id), para poder nomear este formulário para ele ser tornar-se acessível de forma nomeada para um script em JavaScript, que vai ser executado dentro da página. Você escolhe o nome do formulário, eu resolvi usar um prefixo “F_” para saber dentro dos meus fontes que este componente é um form.

Outra escolha importante foi o método do formulário. Eu poderia usar um formulário do tipo GET, mas isto não seria nada elegante para uma tela de login, por várias razões. Primeira, os formulários com método GET colocariam os nomes e valores dos campos input na URL. Isso expõe a senha do usuário na URL. E, os Web Browses costumam fazer cache e permitir Bookmark de URL passando parâmetros via GET. Nada bom para uma operação de autenticação.

Quando usamos POST, o Browse não faz cache dos dados submetidos, e os dados são enviados internamente do Web Browse ao Web Server, dentro do corpo da requisição HTTP.

Links e JavaScript

Existem componentes no HTML, como o button, que permitem a criação de botões em um Web Site. Os botões normalmente disparam ações em JavaScript, e podem ter seu layout alterado usando CSS (ou folhas de estilo). Eu poderia usar botões, porém opter por usar o componente âncora (a) do HTML, usando CSS para dar a aparência de um botão, e internamente usar o evento de disparo (onclick) do componente para chamar uma função JavaScript declarada dentro da minha página, vide exemplo do botão de Login.

<a class="agbutton" id="btnConfirm" href="javascript:void(0)" onclick="doLogin()"

Já o botão para voltar à página principal do site, usa o componente âncora (a) apontando diretamente para a URL raiz do site “/”, sem usar javascript.

<a class="agbutton" id="btnVoltar" href="/">Voltar</a>

Outro detalhe interessante é o uso de CSS. Para você que já ouviu falar nisso, basicamente eu posso criar uma classe de layout, ou estender classes já existentes dos componentes padrão, para mudar a forma de apresentação destes componentes. As definições de estilo são feitas dentro da tag style, onde a definição pode estar dentro do póprio HTML, ente as tuplas <style> e </style>, ou mesmo em um arquivo separado, onde usamos um parâmetro da tag style para indicar onde está o arquivo. No momento, o estilo está dentro da página atual. Assim que acrescentarmos mais páginas ao projeto, colocamos as definições de estilo comuns em um arquivo de estilo separado, e fazemos referência a este arquivo nas páginas da aplicação.

Validação do Login

A ação do formulário, isto é, a URL para a qual a requisição de POST gerada no momento que o formulário for submetido também é importante. No caso, vamos chamar a aplicação login,apw, responsável por receber os campos do POST (Usuário e Senha)  deste formulário, para verificar se o usuário deve ter acesso ou não para a página da agenda. Para isso, acrescentamos mais uma entrada no programa ASPThreads.prw, vide abaixo:

case cAspPage == 'login'
  cReturn := U_WLOGIN()

E, para realizar a tarefa de validação do login, vamos criar o arquivo wlogin.prw, que vai conter a função U_WLOGIN() — e inclusive uma de Logoff.

#include "protheus.ch"

User Function WLogin()
Local cHtml := ''
Local cUser := ''
Local cPass := ''

If HTTPPOST->USER != NIL .and. HTTPPOST->PASS != NIL
  // Houve um POST , informando usuario e senha 
  // Valida usuario e senha informados 
  cUser := Upper(alltrim(HTTPPOST->USER))
  cPass := HTTPPOST->PASS
  If cUser == "ADMIN" .AND. cPass == ""
    // Usuário logado com sucesso 
    // Alimenta a session LOGIN 
    HTTPSESSION->LOGIN := cUser
  Else
    // Informa mensagem de erro usando o alias virtual HTTPPOST
    HTTPPOST->_ERRORMSG := "Usuário ou senha inválidos."
  Endif
Endif

If empty(HTTPSESSION->LOGIN)
  // Usuário ainda não está logado, retorna a tela de login
  cHtml := H_WLogin()
Else
  // Usuário atual está logado. Redireciona ele para a agenda
  HTTPSRCODE(307,"Login Redirection")
  HTTPHEADOUT->Location := "/agenda.apw"
  Return ""
Endif
Return cHtml

/* -------------------------------------------------------
Logoff de Usuario 
Retorna Limpa as variaveis de session do usuario 
e retorna a página de indice do site 
------------------------------------------------------- */
USER Function WLogOff()
HTTPFREESESSION()
return H_INDEX()

Olhando ao mesmo tempo a página wlogin.aph, e a função u_wlogin(), reparem que ambas usaram um campo do alias virtual HTTPPOST, que não estava em nenhum formilário — HTTPPOST->_ERRORMSG — onde a criação deste campo é feita diretamente no alias virtual HTTPPOST, dentro da aplicação, e a verificação da existência e do conteúdo deste campo é feita dentro do arquivo windex.aph.

Sim, eu posso criar em tempo de programação um identificador e um conteúdo, nos alias curtuais HTTPGET e HTTPPOST, e depois veremos outros casos onde isto também é possível. Desta forma, ao invés de precisarmos declarar variáveis PRIVATE dentro de um fonte prw para enviar dados de programa para um aph, criamos um ou mais campos no alias virtual HTTPPOST por exemplo, tomando o cuidado destes campos não conflitarem com nomes de campos vindos do Web Browse ou de formulários.

Como isso vai funcionar — passo a passo

O usuário abre o Web Browse e entra com a URL http://localhost/agenda.apw . A função U_ASPConn() do fonte ASPThreads.prw será chamada, e vai executar a função U_WAgenda()

case cAspPage == 'agenda'
  cReturn := U_WAGENDA()

A função U_WAgenda() vai consultar a HTTPSESSION->LOGIN, que não existe ainda, indicando que o usuário ainda não foi autenticado, retornando para ele o conteúdo do arquivo wlogin.aph . Este arquivo retorna uma página de login centralizada no Web Browse, solicitando informar usuário e senha.

Web Login

Após informar o usuário ADMIN e a senha em branco, e clicar no botão Confirmar, a ação do formulário será submeter os campos digitados em uma requisição POST, para a URL http://localhost/login.apw, que por sua vez vai chamar a função U_WLOGIN()

case cAspPage == 'login'
  cReturn := U_WLOGIN()

Dentro da função U_WLOGIN(), uma vez verificado que houve o envio de parâmetros de POST na requisição, verificado pelo recebimento dos campos USER e PASS, declarados no formulário HTML, caso o usuário e senha informados sejam aptos de realizar a operação, a HTTPSESSION->LOGIN será criada e alimentada com o ID do usuário. Caso contrário, a variável de POST _ERRORMSG será criada com uma mensagem de erro de Login. Uma vez que o usuário esteja logado com sucesso, retornamos para o Browse mediante instruções especiais — que ainda não foram vistas por aqui — para redirecionar o Browse a abrir a página da agenda no link “/agenda.apw”, que por sua vez retornará a página ainda em construção wAgenda.aph.

Web Agenda.png

Revisão de Conceitos

  1. Qualquer página com link .apw  será processada pela função U_ASPCONN(), que recebe o nome da página no alias virtual HTTPHEADIN->MAIN. No nosso caso, existe um “DO CASE” com uma lista de páginas e as suas respectivas funções para chamada.
  2. Qualquer requisição feita do Browse via URL é do tipo GET, e pode passar parâmetros via URL.
  3. Um formulário HTML, ao ser submetido, pode fazer um GET ou um POST, dependendo do método configurado no form.
  4. Um formulário do tipo POST também pode passar parâmetros via URL, colocados no action do formulário.
  5. O Web Browse apenas estabelece uma conexão HTTP com o Application Server para fazer uma requisição, encerrando a conexão automaticamente após o retorno — ou antes do retorno em caso de time-out ou cancelamento da requisição pelo usuário.
  6. Mesmo que você use alguma validação no Client — por exemplo funções JavaScript — para evitar que dados inconsistentes sejam enviados ao Servidor, não deixe de fazer as consistências da recepção dos dados no Advpl ASP. Pessoas mal intencionadas podem tentar submeter conteúdos inválidos para tentar burlar comportamentos, causar danos ou mesmo indisponibilidade de serviço.
  7. Um processamento AdvPL ASP dentro de um arquivo APH deve ser usado para montar conteúdo dinâmico para ser apresentado e/ou processado no Web Browse. Tudo o que estiver entre as tags <% , <%= e %> será processado somente no momento em que o servidor receber a requisição, cujo retorno será enviado ao Web Browse como uma página HTML.

Conclusão

Os posts anteriores sobre AdvPL ASP são a base para esta nova etapa do Crud, recomendo a leitura deles para uma melhor compreensão dos conceitos aqui apresentados, bem como uma leitura extra sobre o protocolo HTTP. A página da agenda ainda não foi publicada, pois está em construção, aguardem que eu ainda não coloquei todos os ingredientes na cumbuca … risos …

Agradeço a todos os comentários, compartilhamentos e likes, e desejo a todos(as) TERABYTES DE SUCESSO !!! 

Referências

 

 

Protheus e AdvPL ASP – Parte 03

Introdução

No post anterior, Protheus e AdvPL ASP – Parte 02, vimos dois alias virtuais, usados para receber parâmetros do Browse, a partir de requisições GET e POST — são eles o alias virtual HTTPGET e HTTPPOST, respectivamente. Agora vamos os demais alias virtuais disponíveis no AdvPL, começando pelo HTTPSESSION.

Alias virtual HTTPSESSION

É possível criar dinamicamente variáveis em um container global do AdvPL ASP, cujo escopo seja a instância do navegador ou Web Browse utilizado — ou em outras palavras,  “sessions de usuário”. Para isso, usamos o alias virtual HTTPSESSION.

O armazenamento no alias virtual HTTPSESSION é feito dinamicamente, na forma de tuplas chave=valor, onde damos o nome a um campo virtual, e atribuímos a ele uma informação. Por exemplo:

HTTPSESSION->USERNAME := "José"

Para consultarmos se um determinado campo virtual existe e/ou têm conteúdo, fazemos referência a ele usando da mesma forma o alias virtual HTTPSESSION, vide exemplo:

IF Empty(HTTPSESSION->USERNAME)
  conout("Session USERNAME vazia ou inexistente")
Else
  conout("Session USERNAME = " + cValToChaR(HTTPSESSION->USERNAME) )
Endif

Como o AdvPL ASP identifica o usuário?

Uma vez que um usuário abra um navegador, e solicite ao Protheus Server uma página qualquer com link .apw, que fará um processamento de AdvPL ASP, um Cookie de Memória (recurso do Web Browse para armazenar uma tupla chave=valor durante a navegação) é usado para identificar a seção (usuário) atual.

Quando você acabou de abrir o Browse, e fez a primeira requisição de link .apw para o AdvPL ASP, o Protheus Server não vai receber este identificador, então ele cria um identificador novo para a seção atual — aquela instância de Web Browse acessando o site — e retorna este identificador ao Web Browse como um “Cookie de Memória”. O Web Browse, por sua vez, a partir deste momento, e enquanto ele estiver aberto, envia de volta esse identificador como uma informação no cabeçalho HTTP de cada requisição GET ou POST que ele fizer para o Protheus Server.

Escopo e Tempo de Vida das Sessions

Uma vez que você atribua um conteúdo para uma variável de session, este conteúdo é gravado na memória da instância atual do Protheus Server, e somente será possível recuperá-lo através de um código AdvPL executado dentro de uma Working Thread do AdvPL ASP, que foi feita a partir da mesma instância de Web Browser, que fez a gravação da informação e respectivo identificador.

Todas as informações (identificadores e conteúdos) gravados para um determinado e distinto usuário, permanecerão na memória do servidor por tempo indeterminado, desde que este usuário não deixe de fazer uma requisição ao Protheus Server em até 10 minutos. Após 10 minutos sem atividade em um conjunto de dados de HTTPSESSION atrelado a um usuário, os identificadores e conteúdos serão descartados — apagados da memória. Isto não muda o identificador interno da seção daquele usuário.

Este tempo de 10 minutos — ou 600 segundos — é o valor DEFAULT da configuração SESSIONTIMEOUT, que permite definir o tempo de permanência máximo por inatividade do conjunto de variáveis de session por usuário — vide links de referência no final do post.

Onde eu uso variáveis de SESSION?

O uso mais comum são propriedades e parâmetros exclusivos que a aplicação permite definir para um ou mais usuários distintos que estão navegando no Web Site ou Aplicação WEB em questão. Por exemplo, um uso muito comum é a identificação de acesso de usuário, ou Login”.

Imagine que várias páginas dinâmicas da aplicação escrita em AdvPL ASP pode ser acessada por qualquer pessoa — acesso público e irrestrito. Porém, determinadas operações feitas através de determinados programas deste Web Site possuem acesso restrito, onde o usuário que estiver navegando deve fornecer algum tipo de informação para identificar-se na aplicação, e tentar garantir que ele “é quem diz ser”.

Nas páginas ou funções onde esta autenticação ou Login é necessária, podemos verificar se uma determinada SESSION — por exemplo HTTPSESSION->LOGIN possui conteúdo. Esta SESSION somente será criada se o usuário passar pelo processo de Login da Aplicação Web, normalmente usando uma página exclusiva na aplicação para esta finalidade. E, em cada função ou página que requer identificação ou é de acesso restrito, caso a SESSION de LOGIN não esteja definida, podemos lhe informar uma mensagem de “Acesso restrito a usuários inscritos”, e direcioná-lo a uma tela de cadastro ou a uma tela de Login.

O Que eu posso guardar em SESSION ?

Nobre desenvolvedor, você armazenar em campos do alias virtual HTTPSESSION qualquer valor básico do AdvPL, EXCETO “B” (Blocos de Código ou Code-Blocks) e “O” (Objetos ou Instâncias de Classe). O resto, inclusive Array, pode.

Agora, preste a atenção no seguinte: Um usuário ou internauta navegando no seu Web Site em AdvPL ASP, pode simplesmente parar de navegar por qualquer razão. E, durante a navegação, cada requisição de URL vinda do Web Browse é atendida por uma conexão estabelecida entre o Web Browse e o Protheus Server, que é encerrada após o processamento e envio dos dados ao Browse. Trata-se de uma conexão não-persistente.

Logo, se você coloca um botão ou link de “LOGOFF” no seu site, e o usuário realmente clica neste botão, você pode disparar uma função dentro do AdvPL para limpar manualmente todas as variáveis de SESSION deste usuário (HTTPFreeSession). Porém, se o usuário não clicar neste botão e simplesmente fechar o Web Browse, toda a memória consumida por aquele usuário, atrelada a um identificador exclusivo da seção que ele estava navegando, ficarão na memória do Protheus Server até que passe os dez minutos de INACTIVETIMEOUT, ou o tempo de inatividade configurado.

Se você, para um determinado usuário, usou 1 MB de memória para armazenar informações de SESSION, esta memória será ocupada por até 10 minutos a mais do que o usuário está realmente usando no Web Site. Ao aumentar o INACTIVETIMEOUT para valores maiores, aumentamos o tempo de retenção dessa memória. Aproveitando este exemplo, de 1 MB de consumo por usuário, e INACTIVETIMEOUT de 30 minutos. Das 12:00 às 12:10, 500 usuários navegaram no site, dos quais 100 entraram na área restrita e usaram SESSIONs. Em 10 minutos, 100 MB de uso de memória. Entre 12:10 e 12:20, entraram mais 50 usuários na área restrita, e 50 usuários que entraram às 12:00 fecharam o browse e foram almoçar. Logo, eu tenho agora (12:20) 100 usuários acessando a área restrita, mas estou mantendo na memória um total de 150 sessions, de todos os usuários que entraram desde às 12:00. Das 12:20 até 12:30 saíram e entraram mais 50 usuários, às 12:30 eu tenho o mesmo volume de 100 usuários online acessando páginas restritas, mas estou usando 200 MB para armazenar 200 variáveis de SESSION, 100 dos usuários ativos no momento, e as outras 100 que foram criadas desde o meio dia por usuários que já saíram do site.

Boas práticas de Sessions

Só existe uma boa prática de sessions: Evite usar sessions para guardar valores para qualquer pessoa navegando na aplicação WEB ou Web Site. Procure usar somente para guardar o que realmente é imprescindível, apenas para os usuários que precisam disso, como por exemplo uma informação de login ou alguma preferência diferenciada entre usuários.

Se você pretende criar uma aplicação WEB em AdvPL ASP, algo cujo tamanho e quantidade de acessos simultâneos não seja suportado por apenas uma instância única do Protheus como servidor WEB, então monte sua aplicação para não usar variáveis de SESSION, ou na verdade até pode usar, mas prefira utilizar uma abordagem que possibilite por exemplo a execução de requisições não exija “afinidade” — aplicações STATELESS por exemplo. Dessa forma, não importa em qual servidor a sua requisição seja processada, você consegue verificar a sua validade sem depender de um contexto. Se você usa variáveis de SESSION e resolve subir mais de uma instância de Protheus Server, usando um proxy reverso ou NLB (Network Load Balance), e uma requisição cria uma variável de SESSION quando foi processada no Servidor 1, caso a próxima requisição vá consultar a existência dessa variável seja direcionada para o Servidor 2, este servidor não conhece as sessions do Servidor 1, e vai tratar a requisição como se a Session realmente não existisse.

Conclusão

Embora este tópico não tenha visualmente um exemplo palpável, ele é necessário para a implementação em AdvPL ASP de outro tópico em desenvolvimento, sobre o CRUD em ADVPL ASP, onde vamos criar e usar uma SESSION para controle de login de usuário.

Por hora, apenas agradeço a todos(as) pela audiência e desejo a todos(as) TERABYTES DE SUCESSO 😀

Referências

Protheus e FTP Client – Parte 02

Introdução

No post anterior (Protheus e FTP Client), vimos um exemplo básico de identificação da existência de um arquivo, e como fazer para baixar o arquivo do FTP em uma pasta local a partir do RootPath do ambiente Protheus. Agora, vamos ver com mais detalhes algumas propriedades interessantes da classe TFTPClient.

Propriedades da classe TFTPClient

Todas as propriedades e métodos da classe estão documentadas na TDN, vide link nas referências, no final do post, porém vamos ver algumas delas uma riqueza maior de detalhes, para entender onde é preciso utilizá-las.

bFireWallMode

Esta propriedade indica se a conexão com o FTP será Ativa ou Passiva. Para esta propriedade ter efeito, ela deve ser setada antes de estabelecermos a conexão com o FTP Server.

Em poucas palavras, o FTP usa duas conexões entre o Cliente e o Servidor FTP, uma conexão de dados e outra de controle. Quando usado o modo Ativo (bFireWallMode = .F.  — DEFAULT) , o FTP Server estabelece a conexão de dados em uma porta aberta e indicada pelo Cliente, que abre primeiro a conexão de controle na porta 21 do FTP Server. Pela perspectiva do Firewall do Cliente FTP, uma sistema externo está tentando conectar-se com um cliente interno, o que normalmente é restrito.

Quando habilitamos a propriedade bFireWallMode para .T., indicamos ao Cliente de FTP que ele deve estabelecer a conexão em “Modo Passivo”. Uma vez que o FTP Server seja capaz de trabalhar com a conexão em modo passivo, o Cliente de FTP abre as duas conexões — controle e dados — no FTP Server, na seguinte sequência: Após abrir a conexão de controle no FTP Server, o cliente informa ao FTP Server que a conexão deve ser passiva, então o FTP Server retorna um número de uma segunda porta — acima de 1024 — para o Cliente abrir a conexão de dados, sem precisar abrir um range de portas no Cliente FTP.

— Observação – O Protheus como FTP Server não suporta o modo passivo.  —

bUsesIPConnection

Esta configuração pode ser necessária quando o servidor onde o Protheus Server está sendo executado possua mais de uma interface de rede. Quando o FTP Client não está em modo passivo, e deve receber a conexão de dados de “volta”do FTP Server, normalmente o FTP Client busca o IP da máquina atual para enviar ao FTP Server, porém quando a máquina têm mais de uma interface de rede, não é garantido que o IP retornado seja por exemplo o IP “Externo”, que aceite a conexão. Quando habilitamos a propriedade bUsesIPConnection para .T., o FTP Client pega o IP da Interface de rede que estabeleceu a conexão de controle com o FTP, para informar ao FTP Server onde ele deve fazer a conexão de dados.

cErrorString

Esta é uma propriedade de consulta. Normalmente os métodos da classe cliente de FTP retornam “0” (zero) em caso de sucesso, e em caso de falha um código de erro. A lista de código de erros está documentada neste link da TDN, porém quando um método retorna erro, a propriedade cErrorString é alimentada com a descrição do erro retornado. Isto facilita a montagem de uma mensagem ou LOG de erro com mais detalhes.

nConnectTimeout

Por default, o time-out de tentativa de conexão com o FTP Server é de 5 segundos. Para alterar este tempo (em segundos) antes de estabelecer a conexão, defina o valor desejado nesta propriedade. Alguns servidores — dependendo da velocidade e latência de rede — podem precisar de um tempo um pouco maior.

nControlPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa a porta que foi usada para a conexão de controle (DEFAULT=21). Se esta propriedade for setada antes da conexão estabelecida, ela define a porta default de conexão com o FTP Server. O método FTPConnect(), usado para estabelecer a conexão, permite opcionalmente receber a porta de conexão no segundo parâmetro. Caso este não seja informado, será usada a porta definida na propriedade nControlPort.

nDataPort

Caso já exista uma conexão estabelecida com o FTP Server, esta propriedade informa qual é a porta TCP do FTP Client usada para estabelecer a conexão de dados. Quando não usamos o modo passivo, o FTP Client usa uma porta randomicamente sorteada entre 10 e 30 mil.

nDirInfo

Esta propriedade, quando consultada, realiza uma busca da lista de arquivos da pasta atual setada na conexão com o FTP Server. retornando em caso de sucesso o valor 0 (zero), caso contrário retorna o código do erro ocorrido. Normalmente realizamos a busca dos arquivos disponíveis na pasta atual usando o método Directory(), que internamente também realiza a busca de arquivos da pasta atual do FTP Server, permitindo inclusive um retorno filtrado por um arquivo específico ou o uso de máscaras (* e ?).

nDirInfoCount

Após uma consulta à propriedade nDirInfo,  a propriedade nDirInfoCount informa quantos arquivos foram encontrados na pasta atual do FTP Server.

nTransferMode

Esta propriedade indica e permite alterar o modo interno de transferência de dados entre o FTP Client e o FTP Server. Este valor deve ser setado após estabelecida a conexão com o FTP Server. Existem três modos de transferência: 0=Stream, 1=Block e 2=Compressed. O modo Stream é o DEFAULT. A RFC 959 explica em detalhes cada um dos modos, porém vejamos uma síntese de cada um.

Stream significa que os dados do arquivo são transmitidos em um modo contínuo, usando algumas sequencias de escape internas de controle, é o modo mais comum de transmissão; Block indica o uso de uma sequência de blocos de dados formatados em cabeçalho e conteúdo; e Compressed indica o uso de um algoritmo simples de controle para transmissão de bytes repetidos — economizando no tráfego de rede quando existem sequências do mesmo byte repetidas no arquivo a ser transmitido — veja mais detalhes sobre Run-length Encoding.

nTransferStruct

Permite alterar o modo como os dados são tratados na transferência. Os modos disponíveis são: 0=File (DEFAULT), 1=Record e 2=Page. A implementação de arquivo (File) é normalmente a mais utilizada, pois não interfere em seu conteúdo. Já as estruturas de transferência orientadas a registro (Record) e página (Page) podem ter interferências e sofrerem ajustes em uma das pontas da conexão, dependendo da plataforma em uso. Recomenda-se o uso da opção default (0=File), salvo em necessidades de integrações específicas.

nTransferType

Permite definir o tipo de transferência de dados usada na conexão. Por padrão, a transferência é do tipo 1=Image. Isto significa que os dados do arquivo são transmotidos em modo binário, isto é, a sqeuência de Bytes que compõe o arquivo, independente de seu conteúdo, é transmitida e recebida sem alteração.

Quando usado o tipo 0=ASCII, feito exclusivamente para a transmissão de arquivos texto (Texto Simples, sem UTF-8 ou caracteres especiais), podem haver alterações do conteúdo do arquivo entre plataformas, inclusive em casos onde isto seja desejável. Por exemplo, um arquivo texto puro no Windows usa dois bytes (CRLF) para indicar final de linha. Ao ser transmitido por FTP para uma estação Linux, usando o modo 0=ascii, as quebras de linha serão salvas no linux apenas com LF. Porém, se você setar o modo ascii e acidentalmente transmitir um arquivo de imagem ou outro arquivo de conteúdo binário, fatalmente ele vai ser “corrompido” no processo de gravação de quem estiver recebendo o arquivo.

Conclusão

Por hora, este post fica apenas como referência destas propriedades. No próximo sobre este assunto, vamos montar algo um pouco “maior” com a classe client de FTP.

Agradeço novamente a audiência, e desejo a todos TERABYTES DE SUCESSO !!! 

Referências

Protheus e FTP Client

Introdução

No post Protheus como Servidor de FTP, vimos como configurar um Servidor Protheus como FTP Server. Agora, vamos ver uma classe AdvPL que permite estabelecer uma conexão com um servidor FTP, e fazer operações como Download e Upload de arquivos — a classe tFtpClient.

Protocolos FTP, FTPS e SFTP

FTP, acrônimo de File Transfer Protocol, é um protocolo criado com a finalidade de transferência de arquivos entre estações. Para isso, uma estação atua como Cliente, estabelecendo uma conexão TCP/IP com a estação servidora. A porta padrão para os servidores de FTP é a porta 21.

O FTPS nada mais é do que uma extensão do protocolo FTP, que acrescenta o suporte a conexão criptografada usando TLS e/ou SSL.

O SFTP normalmente é confundido com o FTPS.  Na verdade seu nome vêm apenas da finalidade de transferência de arquivos, porém sua origem e natureza de implementação é bem diferente do FTP. Acrônimo de SSH File Transfer Protocol, na verdade é uma extensão do protocolo SSH (Secure Shell Protocol)  versão 2.0, concebido para transferência segura de arquivos.

Classe TFTPClient

Através da classe TFTPClient() do AdvPL, podemos criar uma conexão com um servidor FTP, bastando ter em mãos no mínimo o IP — ou nome do host — e a porta do servidor. Caso o servidor exija autenticação, precisamos ter um nome de usuário e uma senha para ser autenticada pelo servidor e realizar as operações.

Vale mencionar que, por hora, a classe TFTPClient() suporta apenas o protocolo FTP. Não há (ainda) suporte nativo em AdvPL para conexão cliente com FTPS e/ou SFTP. Quando existe a necessidade de uma integração automatizada com um servidor de arquivos implementado sobre um destes protocolos, a alternativa atual é executar uma aplicação externa mediante script ou similar, que faça a conexão e as tarefas necessárias.

Funcionalidades

Basicamente, as operações de um Cliente de FTP são as equivalentes a uma navegação em uma estrutura de pastas ou diretórios. Ao estabelecer a conexão com um servidor de FTP, normalmente o nosso diretório de trabalho remoto é a pasta “raiz” de publicação do FTP Server. A partir desse ponto, podemos executar operações como “listar arquivos da pasta atual”, “entrar em uma pasta”, “voltar para a pasta anterior”, “copiar um arquivo da pasta do FTP para a estação atual”, “copiar um arquivo da estação atual para a pasta atual do FTP”, “apagar um arquivo” e assim por diante.

Através das propriedades e métodos da classe TFTPClient(), podemos consultar ou parametrizar — de acordo com as capacidades do Servidor de FTP — o modo de transferência de arquivos, o tipo de transferência, o modo de conexão (Ativo ou Passivo), entre outras particularidades. Será mais fácil entender o funcionamento da classe partindo de um exemplo.

Exemplo AdvPL

O primeiro exemplo de uso será bem simples. Sua função é identificar a existência de um determinado arquivo na pasta Raiz de um servidor de FTP, e caso o arquivo exista, a aplicação AdvPL fará o download deste arquivo para uma pasta local do ambiente (environment) a partir do rootpath, chamada “downloads”. O Servidor de FTP utilizado foi um IIS em um Windows 10, com um site de FTP configurado para permitir acesso anônimo.

#include 'protheus.ch'

User Function TSTFTP()
Local oFtp, nStat
Local aFtpFile 
Local cFtpSrv := 'note-juliow-ssd'
Local nFTPPort := 21

SET DATE BRITISH
SET CENTURY ON

// Cria o objeto Client
oFtp := tFtpClient():New()

// Estabelece a conexão com o FTP Server 
nStat := oFtp:FtpConnect(cFtpSrv,nFTPPort)
If nStat != 0
  conout("FTPClient - Erro de Conexao "+cValToChar(nStat))
  QUIT
Endif

// Procura pelo arquivo leiame.txt
aFtpFile := oFtp:Directory( "leiame.txt", .T. )

if len(aFtpFile) > 0
  // Arquivo encontrado - Mostra os detalhes do arquivo 
  conout( cValToChar(aFtpFile[1][1])+" | "+; // nome
    cValToChar(aFtpFile[1][2])+" | "+; // tamanho
    cValToChar(aFtpFile[1][3])+" | "+; // data
    cValToChar(aFtpFile[1][4])+" | "+; // horario
    cValToChar(aFtpFile[1][5]) ) // Atributo . D = Diretorio
  // Faz o download do arquivo para a pasta local de downloads
  nStat := oFtp:ReceiveFile('leiame.txt','\downloads\leiame.txt' ) 
  If nStat != 0 
    conout("*** Falha no recebimento do arquivo - Erro "+cValToChar(nStat))
  Else
    conout("Arquivo recebido com sucesso.")
  Endif
Else
  conout("*** Arquivo nao encontrado.")
Endif
oFtp:Close()
Return

Caso ocorra falha de conexão, o programa não continua. Ao determinar a existência do arquivo no Servidor de FTP — através do método ::Directory() — fazemos o download do arquivo usando o método ::ReceiveFile(). Caso o arquivo na pasta local já exista, ele será sobrescrito.

Nas referências deste post, verifiquem todas as propriedades e métodos disponíveis na classe TFTPClient() na documentação dela na TDN. Para uma primeira versão, nosso exemplo será bem “arroz com feijão” mesmo, acredito que somente com um programa mais extenso, ou com mais programas de tamanho reduzido, será possível exemplificar as demais funcionalidades da classe TFTPClient()

Observações

Os primeiros testes das funcionalidades básicas foram feitos configurando o programa Cliente de exemplo usando o Protheus como FTP Server. E, para meu espanto, o método ::Directory()  não encontrava o arquivo, na verdade mesmo que a máscara de busca informada fosse  “*”  — para identificar todos os arquivos e sub-pastas a partir da pasta atual, não localizavam nada. Em um dos testes, eu resolvi acessar — para consulta — a propriedade chamada nDirInfo da engine Client de FTP, e para minha surpresa, após acessar esta propriedade, o método ::Directory(“leiame.txt”) localizou o arquivo, e o download / recebimento foi feito com sucesso. Como houveram falhas também em outras funcionalidades da API client, porém somente quando usado o Protheus como FTP Server, por hora os exemplos usados para demonstração das funcionalidades da classe TFTPClient serão testados com o FTP do Microsoft IIS, e posteriormente com um FTP Server no Linux,

Conclusão

Ainda vamos explorar mais esta classe, inclusive acessando um FTP Server na Internet. Porém, esta abordagem fica para o próximo post.

Agradeço novamente a audiência, curtidas, compartilhamentos, comentários e sugestões, e desejo a todos TERABYTES DE SUCESSO 😀

Referências

 

 

Identificando Problemas – Queries lentas – Parte 04

Introdução

Continuando o assunto de identificação de problemas, vamos ver agora o que e como lidar com queries que não apresentam um bom desempenho. Antes de chamar um DBA, existem alguns procedimentos investigativos e algumas ações que ajudam a resolver uma boa parte destas ocorrências.

Queries, de onde vêm?

Quando utilizamos um Banco de Dados relacional homologado com o ERP Microsiga / Protheus, todas as requisições de acesso a dados passam pela aplicação DBAccess. Como eu havia mencionado em um post anterior, o DBAccess serve de “gateway” de acesso ao Banco de Dados, e serve para emular as instruções ISAM do AdvPL em um banco relacional.

Logo, as queries submetidas pelo DBAccess ao Banco de Dados em uso podem ser de de dois tipos:

  • Queries emitidas (geradas) pelo DBAccess para atender a uma requisição ISAM — DBGoTop(), DBGoBottom(), DbSkip(), DBSeek().
  • Queries abertas pela aplicação AdvPL para recuperar dados diretamente do Banco de Dados.

Quando utilizamos o DBAccess Monitor, para realizar um Trace de uma conexão do Protheus com o DBAccess, podemos visualizar as operações solicitadas do Protheus ao DBAccess, e as respectivas queries submetidas ao Banco de Dados.

Como identificar uma query “lenta”?

Normalmente encontramos uma ou mais queries com baixo desempenho quando estamos procurando a causa da demora de um determinado processo. A descoberta acaba sendo realizada durante a análise de um Log Profiler obtido durante a execução da rotina, ou também através de um trace da conexão do DBAccess usando o DBMonitor.

Agora, podemos também usar o próprio DBAccess para gerar um log das queries que demoraram mais para serem abertas pelo Banco de Dados. Basta utilizar a configuração MaxOperationTimer, onde podemos especificar um número de segundos limite, a partir do qual um log de advertência deve ser gerado pelo DBAccess, caso o retorno da abertura de uma Query ultrapasse o tempo definido.

O que é uma query lenta?

Definir lentidão normalmente não está ligado apenas a medida absoluta de tempo, mas sim é relativa a urgência ou necessidade de obter a informação rapidamente, versus a quantidade de informações a serem avaliadas e retornadas.

Por exemplo, quando a aplicação AdvPL executa uma instrução como DBGoto(N), onde N é o número de um registro da base de dados, o DBAccess vai montar e submeter uma Query contra o banco de dados, para selecionar todas as colunas da tabela em questão, onde o campo R_E_C_N_O_ é igual ao número N informado.

SELECT CPO1,CPO2,CPO3,...N FROM TABELA WHERE R_E_C_N_O_ = N

Meu chapa, essa Query deve rodar normalmente em menos de 1 milissegundo no banco de dados, por duas razões: A primeira é que a coluna R_E_C_N_O_ é a chave primária (Primary Key) de todas as tabelas criadas pelo DBAccess, então naturalmente existe um índice no Banco de Dados usando internamente para achar em qual posição do Banco estão gravadas as colunas correspondentes a esta linha. E a segunda é que, apenas uma linha será retornada.

Se não existisse um índice para a coluna R_E_C_N_O_, o Banco de Dados teria que sair lento a tabela inteira, sequencialmente, até encontrar a linha da tabela que atendesse esta condição de busca. Esta busca na tabela de dados inteira sem uso de índice é conhecida pelo termo “FULL TABLE SCAN”.

Agora, imagine um SELECT com UNION de mais quatro selects, finalizado com um UNION ALL, onde cada Query faz SUB-SELECTS e JOINS no Banco de Dados, e o resultado disso não vai ser pequeno … Mesmo em condições ideais de configuração do Banco de Dados, não é plausível exigir que uma operação deste tamanho seja apenas alguns segundos.

Causas mais comuns de degradação de desempenho em Queries

Entre as mais comuns, podemos mencionar:

  1. Ausência de um ou mais índices — simples ou compostos — no Banco de Dados, que favoreçam um plano de execução otimizado do Banco de Dados para recuperar as informações desejadas.
  2. Estatísticas do Banco de Dados desatualizadas.
  3. Picos de CPU , Disco ou Rede, na máquina onde está o DBAccess e/ou na máquina onde está o Banco de Dados.
  4. Problemas de hardware na máquina do Banco de Dados ou em algum dos componentes da infra-estrutura.
  5. Problemas de configuração ou de comportamento do próprio Banco de Dados sob determinadas condições.
  6. Excesso de operações ou etapas do plano de execução, relacionadas a complexidade da Query ou da forma que a Query foi escrita para chegar ao resultado esperado.

Recomendo adicionalmente uma pesquisa sobre “Full Table Scan” e outras causas possíveis de baixo desempenho em Queries. Quanto mais infirmação, melhor. E, a propósito, mesmo que a tabela não tenha um índice adequado para a busca, se ela for  realmente pequena (poucas linhas) , o Banco de Dados internamente acaba fazendo CACHE da tabela inteira em, memória, então um Full Table Scan acaba sendo muito rápido, quando a tabela é pequena. Esta é mais uma razão por que muitas ocorrências de desempenho relacionados a este evento somente são descobertas após a massa de dados crescer representativamente no ambiente.

Troubleshooting e boas práticas

Normalmente as melhores ferramentas que podem dar pistas sobre as causas do baixo desempenho de uma Query são ferramentas nativas ou ligadas diretamente ao Banco de Dados, onde a ferramenta é capaz de desenhar e retornar — algumas inclusive em modo gráfico — o PLANO DE EXECUÇÃO da Query. Neste plano normalmente as ferramentas de diagnóstico informam quando está havendo FULL TABLE SCAN, e quais são as partes da Query que consomem mais recursos no  plano de execução. Algumas destas ferramentas inclusive são capazes de sugerir a criação de um ou mais índices para optimizar a busca dos dados desejados.

Mesmo sem ter uma ferramenta destas nas mãos, normalmente necessária para analisar queries grandes e mais complexas, podemos avaliar alguns pontos em queries menores “apenas olhando”, por exemplo:

  1. Ao estabelecer os critérios de busca — condições e comparações usadas na cláusula WHERE — procure usar diretamente os nomes dos campos, comparando com um conteúdo fixo,  evitando o uso de funções. É clado, vão existir exceções, mas via de regra procure estar atento neste ponto.
  2. Evite concatenações de campos nas expressões condicionais de busca. Imagine que você tem uma tabela com dois campos, e você tem nas mãos, para fazer a busca, uma string correspondendo a concatenação destes dois valores. Muto prático você fazer SELECT X FROM TABELA WHERE CPO1 || CPO2 = ‘0000010100’, certo ? Sim, mas mesmo que você tenha um índice com os campos CPO1 e CPO2, o Banco de Dados não vai conseguir usar o índice para ajudar nesta Query — e corre o risco de fazer FULLSCAN. Agora, se ao inves disso, você quebrar a informação para as duas colunas, e escrever SELECT X FROM TABELA WHERE CPO1 = ‘000001’ AND CPO2  = ‘01000’ , o Banco de Dados vai descobrir durante a montagem do plano de execução que ele pode usar um índice para estas buscas, e vai selecionar as linhas que atendem esta condição rapidinho.
  3. O Banco de Dados vai analisar a sua Query, e tentar criar um plano de acesso (ou plano de execução) para recuperar as informações desejadas o mais rápido possível. Se todas as condições usadas na cláusula WHERE forem atendidas por um mesmo índice, você ajuda o Banco de Dados a tomar a decisão mais rapidamente de qual índice internamente usar, se você fizer as comparações com os campos na ordem de declaração do índice. Por exemplo, para o índice CPO1, CPO2, CPO3, eu sugiro  uma Query com SELECT XYZ from TABELA WHERE CPO1 = ‘X’ AND CPO2 = ‘Y’ AND CPOC3 >=  ‘Z’

Queries emitidas pelo DBAccess

As queries emitidas pelo DBAccess no Banco de Dados para emular o comportamento de navegação ISAM são por natureza optimizadas. Para emular a navegação de dados em uma determinada ordem de índice, o DBAccess emite queries para preencher um cache de registros — não de dados — usando os dados dos campos como condições de busca, na mesma sequência da criação do índice. E, para recuperar o conteúdo (colunas) de um registro (linha), ele usa um statement preparado, onde o Banco faz o parser da requisição apenas na primeira chamada, e as demais usam o mesmo plano de execução.

Porém, isto não impede de uma ou mais queries emitidas pelo DBAccess acabem ficando lentas. Normalmente isso acontece quando é realizada uma condição de filtro na tabela em AdvPL, onde nem todos — ou nenhum —  os campos utilizados não possuem um índice que favoreça uma busca indexada, fazendo com que o Banco acabe varrendo a tabela inteira — FULL SCAN — para recuperar os dados desejados.

Este tipo de ocorrência também é solúvel, uma vez determinado qual seria a chave de índice que tornaria a navegação com esta condição de filtro optimizada, é possível de duas formas criar este índice.

Criando um índice auxiliar

A forma recomendada de se criar um índice auxiliar é acrescentá-lo via Configurador no arquivo de índices do ERP (SIX), para que ele seja criado e mantido pelas rotinas padrão do sistema. Porém, para isso este índice não pode conter campos de controle do DBAccess no meio das colunas do índice, e para se adequar ao padrão do ERP, seu primeiro campo deveria sempre ser o campo XX_FILIAL da tabela.

Quando esta alternativa não for possível, existe a possibilidade de criar este índice diretamente no Banco de Dados. Porém, a existência desse índice não deve interferir na identificação de índices de emulação ISAM que o DBAccess faz quando qualquer tabela é aberta. Para isso, a escolha do NOME DO ÍNDICE é fundamental.

Um índice criado diretamente no Banco de Dados para estes casos deve ter um nome que seja alfabeticamente POSTERIOR aos índices declarados no dicionário de dados (SIX). Por exemplo, existe uma tabela ABC990, que possui 8 índices. O ERP Microsiga nomeia os índices da tabela no padrão usando o nome da tabela e mais um número ou letra, em ordem alfabética. Logo, os oito primeiros indices da tabela chamam-se, respectivamente, ABC9901, ABC9902 … ABC9908.

Nomeie seu primeiro índice customizado com o nome de ABC990Z1. Caso seja necessário mais um índice, ABC990Z2, e assim sucessivamente. Chegou no 9, precisa de mais um índice, coloque a letra “A” — ABC990ZA. Dessa forma, estes índices ficarão por último na identificação do DBAccess, e por eles provavelmente não se encaixarem no padrão do ERP, você não vai — e não deve — usá-los dentro de fontes customizados AdvPL — Estes índices vão existir apenas no Banco de Dados, para favorecer a execução de queries especificas ou filtros específicos de navegação.

Precauções

Uma vez que um ou mais índices sejam criados dentro do Banco de Dados, sem usar o dicionário ativo de dados (SIX), qualquer alteração estrutural na tabela feita por uma atualização de versão ou outro programa pode apagar estes índices a qualquer momento, caso seja necessário, e isso vai novamente impactar o desempenho da aplicação. Para evitar isto, é possível escrever uma rotina em AdvPL — customização — para verificar usando Queries no banco de dados se os índices auxiliares criados por fora ainda existem, e até recriá-los se for o caso. Pode ser usado para isso o Ponto de Entrada CHKFILE() , junto das funções TCCanOpen() — para testar a existência do índice — e TcSqlExec() — para criar o índice customizado usando uma instrução SQL diretamente no banco de dados.

Outras causas de degradação de desempenho

Existem ocorrências específicas, que podem estar relacionadas desde a configuração do Banco de Dados, até mesmo problemas no mecanismo de montagem ou cache dos planos de acesso criados pelo Banco para resolver as Queries. Outas ocorrências podem estar relacionadas a execução de rotinas automáticas ou agendadas no próprio servidor de Banco de Dados. Normalmente o efeito destas interferências são percebidos como uma lentidão momentânea porém generalizada, por um período determinado de tempo.

Conclusão

Use a tecnologia ao seu favor, e em caso de pânico, chame um DBA! Muitas vezes existe uma forma mais simples ou com menos etapas para trazer as informações desejadas. Outras vezes é mais elegante, rápido e prático escrever mais de uma query menor, do que uma Super-Query-Megazord.

Desejo a todos um ótimo final de semana, e muitos TERABYTES DE SUCESSO 😀

Referências