Criptografia em AdvPL – Parte 02

Introdução

No primeiro post da série sobre criptografia e hash (Criptografia em AdvPL – Parte 01), vimos uma breve introdução ao assunto, a lista de funções disponíveis no AdvPL para trabalhar com isso, e um exemplo de uso dos hashes MD5 e SHA*. Agora, vamos ver o hash com mais detalhes sobre cada um deles, e o que cada função permite fazer.

Vimos até agora superficialmente os hashes MD5 e SHA, mas existem vários outros algoritmos para fins específicos, como o RIPE Message Digest 160 Bits (RIPEMD160), que é considerado um hash mais “fraco”, utilizado em projetos onde o nível de segurança é aceitável. Vamos começar pelo MD5 e logo em seguida pelo SHA1.

MD5 message-digest algorithm

Criado por Ronald Rivest em 1991 para substituir uma função anterior de hash (MD4), cuja especificação foi publicada em 1992 na RFC 1321. Para os mais curiosos, veja nesse link o fonte em C da implementação do algoritmo MD5, e demais links de referência no final do post.

Segurança do HASH MD5

Embora o hash gerado tenha 128 bits — o que pode gerar 2^128 combinações distintas (aproximadamente 3,40e+38 combinações possíveis), a base do seu algoritmo hoje é considerada “fraca”, pois foram descobertas formas de ataque que permitem em tempo computacional ridiculamente pequeno forçar uma colisão de hash alterando ou injetando informações nos dados originais usados para gerar o HASH.

Onde isso impacta ?

Em diversos mecanismos de segurança de arquivos e assinaturas digitais, um HASH pode ser usado para garantir a integridade do conteúdo. Por exemplo, em vários sites de download um programa oficial é fornecido diretamente em um link, e logo abaixo é fornecido um HASH MD5, SHA1 ou outro, referente ao conteúdo disponibilizado. Com isso, após fazer o download, você pode executar uma ferramenta do sistema operacional ou um utilitário que calcule novamente o HASH do arquivo baixado. Se o HASH for diferente, ou o arquivo está corrompido, ou pode ter sido adulterado.

Em assinaturas digitais e chaves de criptografia, é fundamental que a chave não seja alterada por ninguém, pois isso permitiria alterar atributos da chave ou injetar código malicioso em um programa. Usando um algoritmo de busca por colisão, eu posso pegar um arquivo qualquer, acrescentar conteúdo nele, e temperar uma parte desse conteúdo para fazer com que o HASH gerado desse novo conteúdo seja idêntico ao HASH gerado pelo conteúdo inicial.

Com isso um certificado digital pode ser “forjado” a partir de um certificado original, e enganar o mecanismo de autenticação do certificado — veremos mais para a frente como isso funciona — mas na prática, você pode acessar um website com SSL, onde o Browser vai achar que o certificado é válido e que você está em uma conexão segura. Golpes como criar um site falso de um banco e pegar seus dados bancários podem ser criados usando  um certificado digital falso que o Browse acredita ser autêntico e verificado, e fazer você realmente acreditar que está acessando o site verdadeiro do banco, ou o sistema operacional acreditar que está usando um software original e autenticado, fornecido pelo próprio fabricante 😛

Entre 2005 e 2008 foram feitas diversas descobertas sobre as vulnerabilidades de colisão de MD5 e SHA1, e em 2008 uma equipe de pesquisadores usou um cluster de PlayStations 3 para forjar um certificado autenticado pela RapidSSL, e a partir dele poder criar outros certificados. Desde então não foram mais emitidos certificados com validação de autenticidade baseadas em MD5. EM 2012 foi identificado um malware chamado Flame, que forjou um certificado digital da Microsoft, fazendo os usuários acreditarem que estavam executando um programa autêntico da mesma. Em 2013 a Microsoft anunciou o fim do suporte a certificados digitais que usam autenticação com o MD5. Também suscetível a ataques desta natureza, as autenticações usando SHA1 também estão sendo descontinuadas, a Microsoft anunciou em 2013 que não recomenda o uso de SHA1 para esta finalidade, e informou para as autoridades certificadoras que deixará de reconhecer certificados com este algoritmo a partir de 2016.

Mesmo assim, MD5 e SHA1 ainda são bastante utilizados em outras finalidades, que não dependem de um nível de segurança maior, quando não compensa o esforço de tentar quebrá-lo.

SHA1 – Secure Hash Algorithm 1

Desenvolvido pela NSA (Agência de Segurança Nacional Americana), publicado na RFC 3174, o SHA1 retorna um código de 20 bytes (160 bits), correspondente a 40 caracteres em representação hexadecimal. Embora use um mecanismo diferente do MD5, o SHA1 também está sujeito a ataques de colisão, e não é recomendável seu uso em chaves criptográficas e certificados, e seu uso nestas aplicações vem sido descontinuado. Por outro lado, ele ainda é amplamente utilizado em outros fins, como determinar por exemplo se um arquivo está íntegro ou foi alterado, como por exemplo os softwares e plataformas de versionamento de fontes, como o GIT, por exemplo.

Funções AdvPL MD5() e MD5FILE()

Documentada no TDN nos links http://tdn.totvs.com/display/tec/MD5 e http://tdn.totvs.com/display/tec/MD5File, a função recebe uma string de tamanho variável, até o limite estabelecido para Strings em AdvPL, e retorna por default uma nova string de 32 caracteres contendo o código hash gerado, representado em hexadecimal. A string informada como parâmetro inclusive pode ser vazia, e pode conter bytes/caracteres de 0 a 255. Como cada byte informado na string de parâmetros é importante e significativo, uma string com um espaço em branco a mais na direita é diferente de uma sem este espaço, e cada uma vai gerar um hash diferente, por qualquer método utilizado. A função possui um segundo parâmetro, opcional, que quando informado com o número 1, faz a função retornar uma string de 16 bytes contendo a representação do HASH calculado.

Ela pode receber uma string originalmente de até 1 MB — tamanho máximo de uma variável “C” Caractere do AdvPL — mas este limite pode ser estendido usando a configuração MaxStringSize do TOTVS Application Server. Caso seja necessário gerar um HASH de um arquivo, você não precisa carregá-lo na memória para fazer isso, basta usar a função MD5File()

Um exemplo de uso da função já foi publicado anteriormente aqui no Blog, no uso do Banco de Dados para armazenamento de imagens em disco. Junto da imagem eu posso salvar o MD5 de seu conteúdo, e caso eu receba uma nova imagem para atualização, eu posso optar por não atualizá-la caso o MD5 gerado sobre o conteúdo da imagem seja o mesmo, veja links de referência no final do post.

No post sobre O que é CODEPAGE e ENCODING – Parte 04, eu menciono uma função do Framework AdvPL muito útil para determinar conteúdos binários dentro de strings, chama-se HexStrDump(). 

Função AdvPL SHA1()

Documentada na TDN no link http://tdn.totvs.com/display/tec/SHA1, ela recebe os mesmos parâmetros da MD5(), e aceita uma string de parâmetro com bytes de ASCII 0 a ASCII 255, retornando por default uma string com 40 caracteres em hexadecimal, correspondendo aos 20 bytes (126 Bits) do hash calculado.

Outras representações

Por padrão a maioria as funções do AdvPL retorna por default a representação do hash calculado em uma string hexadecimal, que ocupa duas vezes mais espaço do que efetivamente os bytes que compõe o hash. Porém, guardar um resultado de HASH em bytes em um campo de uma tabela em um Banco de Dados somente seria possível se fosse usado um campo do tipo “M” Memo, que aceita conteúdo binário. Porém, existem formas menores de representação que o hexadecimal. Por exemplo, podemos calcular um HASH SHA1 e parametrizar a função para retornar os 20 bytes correspondentes ao HASH, e depois codificar o resultado em BASE64 — assim o hash calculado poderia ser gravado em um campo de texto / caractere em apenas 24 caracteres, e não 40 caso ele fosse representado em hexadecimal, veja exemplo abaixo:

User Function SHaToB64()
Local cInfo := 'Hello AdvPL'
Local cSHA1 , cBase64

conout('String original')
conout(hexstrdump(cInfo))

cSha1 := SHA1(cInfo,1) // retorno de string em bytes 
conout('SHA1 em bytes')
conout(hexstrdump(cSha1))

cBase64 := Encode64(cSha1)
conout('Convertido para BASE64')
conout(hexstrdump(cBase64))

Return

// Resultado no console

String original
HexSTRDump  ( String 11 / Start 1 / Length 11 )
-------------------------------------------------------------------------------
48 65 6C 6C 6F 20 41 64 76 50 4C                 | Hello AdvPL
-------------------------------------------------------------------------------

SHA1 em bytes
HexSTRDump  ( String 20 / Start 1 / Length 20 )
-------------------------------------------------------------------------------
D7 E0 A3 66 D6 F2 62 C9 BC A0 F3 FE 89 0C 06 F9  | ╫αúf╓≥b╔╝á≤■ë__∙
B8 76 F6 ED                                      | ╕v÷φ
-------------------------------------------------------------------------------

Convertido para BASE64
HexSTRDump  ( String 28 / Start 1 / Length 28 )
-------------------------------------------------------------------------------
31 2B 43 6A 5A 74 62 79 59 73 6D 38 6F 50 50 2B  | 1+CjZtbyYsm8oPP+
69 51 77 47 2B 62 68 32 39 75 30 3D              | iQwG+bh29u0=
-------------------------------------------------------------------------------

No final das contas, precisamos utilizar os métodos de hashing e codificações exigidos por quem implementa uma determinada funcionalidade.

Conclusão

Ainda tem muito pela frente, mas eu acredito que a partir daqui cada nova etapa deve ficar mais clara.

Espero que estas informações lhes sejam úteis, e lhes desejo como sempre TERABYTES DE SUCESSO !!!

Referências

Copyrights

 *** MD5 message-digest algorithm
 *** Retrieved from https://people.csail.mit.edu/rivest/Md5.c
 
 **********************************************************************
 ** Copyright (C) 1990, RSA Data Security, Inc. All rights reserved. **
 **                                                                  **
 ** License to copy and use this software is granted provided that   **
 ** it is identified as the "RSA Data Security, Inc. MD5 Message     **
 ** Digest Algorithm" in all material mentioning or referencing this **
 ** software or this function.                                       **
 **                                                                  **
 ** License is also granted to make and use derivative works         **
 ** provided that such works are identified as "derived from the RSA **
 ** Data Security, Inc. MD5 Message Digest Algorithm" in all         **
 ** material mentioning or referencing the derived work.             **
 **                                                                  **
 ** RSA Data Security, Inc. makes no representations concerning      **
 ** either the merchantability of this software or the suitability   **
 ** of this software for any particular purpose.  It is provided "as **
 ** is" without express or implied warranty of any kind.             **
 **                                                                  **
 ** These notices must be retained in any copies of any part of this **
 ** documentation and/or software.                                   **
 **********************************************************************

SHA-1

RFC 3174           US Secure Hash Algorithm 1 (SHA1)      September 2001


Full Copyright Statement

   Copyright (C) The Internet Society (2001).  All Rights Reserved.

   This document and translations of it may be copied and furnished to
   others, and derivative works that comment on or otherwise explain it
   or assist in its implementation may be prepared, copied, published
   and distributed, in whole or in part, without restriction of any
   kind, provided that the above copyright notice and this paragraph are
   included on all such copies and derivative works.  However, this
   document itself may not be modified in any way, such as by removing
   the copyright notice or references to the Internet Society or other
   Internet organizations, except as needed for the purpose of
   developing Internet standards in which case the procedures for
   copyrights defined in the Internet Standards process must be
   followed, or as required to translate it into languages other than
   English.

   The limited permissions granted above are perpetual and will not be
   revoked by the Internet Society or its successors or assigns.

   This document and the information contained herein is provided on an
   "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING
   TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
   BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION
   HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

Imagens PNG em AdvPL – Parte 02

Introdução

No post anterior (Imagens PNG em AdvPL) vimos a primeira prova de conceito de leitura de um PNG monocromático. Agora, vamos ver como calcular o CRC de cada chunk, partindo de um fonte em C — e convertendo ele para AdvPL. Recomendo fortemente a leitura do post anterior como base para esta publicação.

CRC

CRC — do inglês Cyclic redundancy check — é um código de verificação de erro normalmente usado em redes digitais e dispositivos de armazenamento para detectar alterações acidentais nos dados. Trata-se da aplicação repetida (ou cíclica) de uma divisão polinomial para uma sequencia de dados, onde o resultado é acrescentado junto dos dados.

A especificação do CRC parece complexa, mas a sua aplicação fica bem mais fácil quando temos em mãos um exemplo eficiente de cálculo, mesmo que esteja em outra linguagem. Como foi o caso do CRC aplicado a cada chunk do arquivo PNG. Vejamos um código em C, dado como exemplo de cálculo do CRC de um determinado buffer. — código extraído do site http://www.libpng.org, vide referências no final do post.

/* Table of CRCs of all 8-bit messages. */
   unsigned long crc_table[256];
   
   /* Flag: has the table been computed? Initially false. */
   int crc_table_computed = 0;
   
   /* Make the table for a fast CRC. */
   void make_crc_table(void)
   {
     unsigned long c;
     int n, k;
   
     for (n = 0; n < 256; n++) {
       c = (unsigned long) n;
       for (k = 0; k < 8; k++) {
         if (c & 1)
           c = 0xedb88320L ^ (c >> 1);
         else
           c = c >> 1;
       }
       crc_table[n] = c;
     }
     crc_table_computed = 1;
   }
   
   /* Update a running CRC with the bytes buf[0..len-1]--the CRC
      should be initialized to all 1's, and the transmitted value
      is the 1's complement of the final running CRC (see the
      crc() routine below)). */
   
   unsigned long update_crc(unsigned long crc, unsigned char *buf,
                            int len)
   {
     unsigned long c = crc;
     int n;
   
     if (!crc_table_computed)
       make_crc_table();
     for (n = 0; n < len; n++) {
       c = crc_table[(c ^ buf[n]) & 0xff] ^ (c >> 8);
     }
     return c;
   }
   
   /* Return the CRC of the bytes buf[0..len-1]. */
   unsigned long crc(unsigned char *buf, int len)
   {
     return update_crc(0xffffffffL, buf, len) ^ 0xffffffffL;
   }

Então, como assim ?!

Entre todas as diferenças da linguagem C para o AdvPL, de sintaxe a tipos de dados, isso realmente não importa. O que importa é eu ter uma forma de fazer a mesma coisa, os mesmos cálculos em outra linguagem. Vamos por partes :

Primeiro, o programa em C cria um array com 256 posições, como uma tabela pré-calculada de CRC, armazenado no array global crc_table, um array de números inteiros de 32 bits com 256 posições — do 0 ao 255.

Uma parte do código cria o conteúdo para estes elementos — usando a função make_crc_table() — realizando uma série de operações de comparação binária AND ( & ) , XOR ( ^ ) e RIGHT BIT SHIFT ( >> ). Depois, a geração do CRC inicializa o número com os 32 bits “ligados”, e aplica o resultado da fórmula polinomial pré-calculada para cada byte da sequência, gerando o CRC. Agora, para converter isso pro AdvPL, vamos por partes

Operações em C e funções equivalentes em AdvPL

Usamos array dinâmico em AdvPL, e podemos armazená-lo dentro de uma variável STATIC no AdvPL para utilização posterior. A operação binária AND ( & ) pode ser feita com a função AdvPL NAND(), o XOR ( ^ ) pode ser feito com a função AdvPL NXOR(), o RIGHT BIT SHIFT equivale a dividir o número decimal por dois considerando apenas a parte inteira do resultado — INT(X/2). E, todos os números constantes em hexadecimal no código podem ser convertidos para números constantes em base decimal no AdvPL.

Geração da tabela de CRC — 256 elementos

Como podemos usar esta tabela várias vezes dentro do mesmo programa, para cada Chunk — de todos os tipos — seja para verificar a integridade do arquivo na abertura como para criar um PNG — vamos armazená-lo em uma variável STATIC em AdvPL, que persiste pela vida do processo dentro do fonte atual.

STATIC aCRCTable := CRCTable()

Agora, dentro do mesmo fonte, vamos criar a função de carga como uma STATIC FUNCTION

STATIC Function CRCTable()
Local aTable := {}
Local nI, nJ , C

For nI := 0 to 255
  C := nI
  For nJ := 0 to 7
    IF nAnd(C,1)
      C := nXor( 3988292384 , Int( C / 2 ) )
    Else 
      C := Int( C / 2 )
    Endif
  Next
  aadd(aTable,C)
Next
Return aTable

Com isso, na carga desse fonte a tabela de constantes será gerada e mantida na memória até o final da thread/processo. Agora, vamos fazer a função que recebe uma String e calcula o CRC.

STATIC Function PNGCRC(cBuffer)
Local C := 4294967295 // 0xFFFFFFFF
Local nI , nASC , nIndex

For nI := 1 to len(cBuffer)
  nASC := asc(substr(cBuffer,nI,1))
  nIndex := nAnd ( nXor( C , nASC ) , 255 ) // 0xff
  C := nXor ( aCRCTable[nIndex+1] , INT( C / 256) ) // C >> 8
Next

Return nXor( C , 4294967295 ) // 0xFFFFFFFF

As diferenças adicionais do código original em C são por conta de usar um array dinâmico em AdvPL, e justamente o Array em AdvPL é base 1 — o primeiro elemento é o índice 1, enquanto em C o primeiro elemento é índice 0 (zero). Logo, quando usamos a tabela, calculamos o valor da posição da tabela da mesma forma que na linguagem C, porém ao acessar o elemento da tabela, somamos 1 no índice. Voilá 😀

Não precisamos do flag “crc_table_computed”, pois a carga do fonte pela thread é feita apenas uma vez, e a geração da tabela é feita automaticamente e apenas uma vez por thread/processo, e não precisamos de duas funções (CRC e UPDATE_CRC) só para encapsular o processo, basta uma função inicializando o CRC com o valor cheio em 32 bits 4294967295 (0xFFFFFFFF em hexadecimal) e fazendo o NXOR() no retorno .Com isso, temos uma forma muito rápida de calcular ou validar o CRC dos Chunks do arquivo PNG.

Conclusão

Você pode fazer a mesma coisa em linguagens diferentes, de jeitos diferentes, e chegar ao mesmo resultado. No próximo post da sequência, vamos ver como encaixar o suporte ao formato PNG de forma elegante na classe de imagens zBitmap 😀

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

 

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

 

Identificando Problemas – Congelamento e Conexões Presas – Parte 03

Introdução

No post anterior (https://siga0984.wordpress.com/2018/11/07/identificando-problemas-congelamento-e-conexoes-presas-parte-02), demos uma boa olhada sobre travamentos e congelamentos, desde a percepção do usuário, até algumas possíveis causas e alguns procedimentos de diagnóstico. Neste post, vou apresentar mais algumas possibilidades, e complementar alguns casos já vistos, e ver mais de perto o “temível” DEADLOCK 😀

Dicas para Todos os Casos

  1. Começamos procurando o processo no Protheus Monitor,e verificando se o total de instruções está sendo atualizado. Se não estiver sendo atualizado, o processo está esperando por “algo”.
  2. O processo tem conexão com o DBAccess? Verifique o que a conexão está fazendo. Se o DBAccess está “IDLE” faz algum tempo, seja lá o que o processo estiver esperando, não é um retorno do DBAccess. Elimine a conexão do DBAccess e espere ela sair do DBAccess monitor — isso pode demorar até 30 segundos, inclusive devido ao fato da aplicação não estar fazendo requisições para o DBAccess, ele somente verifica o flag de “derrubar a conexão” em intervalos de 30 segundos.
  3. O processo ainda está com o SmartClient aberto? Se tiver algum problema no SmartClient, e o server está esperando algo que deveria vir do SmartClient, derrubar o SmartClient também faria o processo terminar — porém com uma mensagem de erro de sincronismo, sem gerar log. Deixemos isso como ultima alternativa.
  4. Podemos também tentar derrubar o processo pelo Monitor do Protheus, mas lembre-se de não usar a opção “derrubar imediatamente”, senão o processo some do monitor, e você somente vai saber se ou quando ele saiu, depois de verificar o console.log do Application Server.
  5. Ao investigar ocorrências estranhas e com poucas pistas, procure obter mais informações, inclusive verifique os logs e configurações das aplicações envolvidas — DBAccess, LockServer (linux), License Server, Protheus Master, Slave(s), etc. — principalmente verifique se nestes serviços não está acontecendo algum ACCESS VIOLATION e FAILURE ON THREAD DELETE. Depois de ocorrências desta natureza, o comportamento da aplicação é imprevisível — mas normalmente os efeitos mais comuns são: Recursos bloqueados ou em uso por um processo que não está mais no Monitor, crescimento do uso da memória ao longo do tempo, inclusive congelamentos.

NÃO ACHEI … E AGORA ?

Beleza, você já olhou com uma lupa e não achou onde travou, ou pior, cada hora trava em um lugar diferente, só acontece na produção, ninguém reproduz no ambiente de desenvolvimento ou na homologação …

Abra um chamado na TOTVS, forneça os detalhes pertinentes, o analista de suporte pode pedir mais algumas informações, e se mesmo assim não for descoberto a causa ou o que está acontecendo, ainda assim é possível usar uma build Debug ou RWD (Release com informações de Debug) do Application Server, fornecida para a análise desta ocorrência, junto com um procedimento para gerar um “Core Dump” manualmente do Protheus Server, ou da aplicação em questão — no momento em que o travamento for reproduzido.

Através de um “Core Dump” gerado nestas condições, o time de Tecnologia consegue abrir este arquivo para análise, e determinar onde e o que cada processo dentro do servidor de aplicação estava fazendo no momento que o Dump foi gerado. Isso ajuda muito no diagnóstico, quando os demais procedimentos não deram resultados satisfatórios.

Outros Casos

Lembrando o caso clássico de “Impressão no Servidor” usando um Driver de geração de PDF, rodando o Protheus como serviço do Windows … O processo atual simplesmente TRAVA dentro do servidor. Este caso está bem detalhado no primeiro post — https://siga0984.wordpress.com/2015/08/01/identificando-problemas-memoria-no-advpl-parte-01/ — vale a pena dar uma lida nele, pois além de travar ele mantém vários recursos ocupados e abertos, como a conexão com o DBAccess , License Server, c-Tree, etc.

Existe também a possibilidade de haver algum erro de lógica ou falha de tratamento de eventos ou um estado de interface não previsto, onde o Loop ou o Travamento pode estar dentro do Application Server, ou mesmo dentro do SmartClient, disparados por alguma condição particular. São erros mais difíceis de serem diagnosticados, principalmente quando não existe — ou ninguém sabe como faz — uma receita de bolo para fazer o problema “aparecer” e ser reproduzido. Reproduzir bug em cativeiro é “de rosca”… Não tem como fugir das etapas do processo investigativo, e se nada deu certo, quando a ocorrência chegar até esta camada, cada caso é estudado individualmente no atendimento, onde outras medidas podem ser adotadas, desde uma build Debug, até uma build com uma instrumentação específica para levantar mais informações sobre a ocorrência pode ser fornecida para o cliente no ambiente em questão.

Outros tipos de Loop Infinito – O DEADLOCK

Esse é um dos tipos de ocorrência que dá mais trabalho de investigar, e seus efeitos são desastrosos … A aplicação AdvPL realiza as alterações de registro na base de dados obrigatoriamente solicitando um Lock de Registro, tratado pelo DBAccess. No ERP, usamos a função RecLock(), do FrameWork AdvPL, que possui um tratamento de retry para a obtenção do bloqueio.

Porém, uma vez que uma determinada aplicação esteja em JOB — Como um Scheduler ou um WebService, por default este retry é reiniciado em caso de falha. Caso dois processos diferentes tenham obtido cada um um determinado lock, e no momento atual um processo tenta obter o lock do registro que está com o outro processo, e vice-versa, temos um DEADLOCK na aplicação AdvPL.

Neste caso, se os dois programas estão em JOB — sem interface — ambos ficam tentando pegar cada um o lock que está com o outro processo, e como nenhum deles vai “desistir”, ambos ficam em loop até que um deles seja identificado e derrubado — pelo Monitor do Protheus ou do DBAccess.

Identificando os processos envolvidos

Normalmente dois ou mais processos entram em loop, fazendo várias tentativas de bloqueio de registro, e ninguém sai do lugar. Nestes casos, como eu não sei o que está acontecendo, uma das alternativas é verificar no DBAccess os processos com transação aberta a muito tempo — existe uma coluna nova para indicar isso — e então, usando o DBAccess Monitor, fazemos um TRACE de alguns segundos da conexão, para ver se ela está tentando pegar um lock e não está conseguindo. Depois de saber a tabela e registro envolvidos, você pode procurar quem é o dono do lock no Monitor de Locks do DBAccess, e ver o que esta conexão esta fazendo. Se ela também está tentando pegar outro lock, isso pode indicar um cenário de deadlock, onde basta chutar um dos processos para que o outro tenha a chance de ser finalizado.

WebServices e DEADLOCKs

Os WebServices do Protheus possuem uma configuração especial para fazer com que o retry para obter o lock seja executado apenas por um período de tempo determinado, e em caso de falha, o JOB do WEBSERVICE é encerrado com uma ocorrência de error.log, indicando que não foi possível obter o bloqueio de um determinado registro de uma tabela, inclusive fornecendo detalhes de qual era o processo que estava “segurando” este lock. A configuração chama-se ABENDLOCK=1, definida na seção de configuração das WORKING THREADS dos WEBSERVICES. De qualquer modo, a partir de Dezembro de 2017, esta configuração foi habilitada por DEFAULT nos WebServices, vide a nota de release da TDN.

DBAccess e DEADLOCKs

Devido a dificuldade de identificar os processos e registros envolvidos em uma ocorrência de DEADLOCK, seria muito interessante se o próprio DBAccess conseguisse identificar uma situação como essa, e avisar a um dos programas envolvidos que ele está envolvido em um DEADLOCK com um ou mais processos, onde a aplicação pode tratar a mensagem do DBAccess e finalizar-se automaticamente, gerando o log de erro correspondente e soltando os bloqueios obtidos, ou deixar que o DBAccess finalize uma das conexões automaticamente, para que as outras tenham chance de terminar.

Conclusão

Por hora, deixo as conclusões com vocês, eu apenas vou concluir este POST 🙂

Agradeço a todos novamente pela audiência, e desejo a todos TERABYTES de SUCESSO 😀

 

 

Identificando Problemas – Congelamento e Conexões Presas – Parte 02

Introdução

No primeiro post sobre identificação de problemas — Identificando Problemas  – Memória no AdvPL – Parte 01 — falamos sobre uso de memória e Leaks de memória. Hoje, vamos obter mais detalhes sobre travamentos, congelamentos, conexões e licenças “presas”, e ocorrências desta natureza.

IGH, TRAVOU…

Bem, um operador do ERP executa uma rotina ou sub-rotina qualquer, normalmente através do SmartClient, certo? Por sua vez, o SmartClient em si é uma aplicação em C++ que, em poucas palavras, foi feita para desenhar os componentes que a aplicação AdvPL criou dentro de uma caixa de diálogo ou Janela, e uma vez que a janela torna-se ativa — método ::Activate() do diálogo — os controles de entrada de dados e interação com a aplicação estão do lado do cliente, e o servidor aguarda pelo disparo de ações a partir dos componentes de tela, como clicar em um botão ou preencher um campo com dados.

Já o Protheus Server, executando um programa AdvPL, ao receber uma ação do SmartClient, executa o bloco de código correspondente a ação, que pode chamar rotinas e sub-rotinas, interagir com a interface atual, e até mesmo montar uma nova caixa de dialogo sobre a interface atual e torná-la ativa.

Do lado do usuário, no SmartClient, a percepção do usuário de “travamento” ou “congelamento” normalmente é percebida como “eu apertei um botão, que deveria fazer X, e nada aconteceu, e eu não consigo clicar ou fazer mais nada”.

Onde, quando, como e o quê travou ?

A resposta de cada uma destas perguntas leva para a próxima pergunta. Onde travou, a informação de qual botão em qual tela que foi apertado e percebido o travamento. Quando, é a informação sobre a periodicidade que isso ocorre. Sempre trava ao apertar este botão? Se não trava sempre, como faz para ele travar? Somente trava quando um campo da tela estava com o valor Y? Ou é uma ocorrência esporádica?  O botão funciona o dia inteiro, e de repente trava…

A resposta da última pergunta é a questão de um milhão …  risos … existem muitas coisas que podem ter acontecido. Vamos enumerar aqui boa parte das possibilidades.

Loop Infinito

Aquele botão dispara uma rotina, com um determinado conjunto de parâmetros, e existe um erro de lógica na rotina, onde uma parte do código entra em loop, realizando um determinado processamento e esperando por uma determinada condição para finalizar o loop. Por exemplo, a rotina abriu uma Query, e está lendo campos da Query e acrescentando um valor em uma variável, até que a Query termine, mas dentro do laço o programador esqueceu de colocar um DBSkip() — ou fez esta operação sem querer em um outro ALIAS, ao invés de fazer na QUERY —  ou ainda um laço FOR … NEXT que usou uma variável N, que inadvertidamente foi alterada dentro do loop, para um outro valor menor, fazendo com que o laço não termine.

Nestes casos, ao abrir o Monitor de Processos do Protheus, e localizar o usuário, a coluna que indica o número total de instruções está sempre crescendo, normalmente a CPU fica mais alta no serviço do Protheus que está executando este processo, e o número de instruções por segundo mostrado no Monitor do Protheus também é alto.

Esse é um caso simples de descobrir onde está o problema, basta finalizar o processo pelo Monitor do Protheus, preferencialmente sem marcar a opção “derrubar imediatamente”. A aplicação AdvPL em loop vai perceber entre uma instrução e outra que ela foi marcada para ser finalizada — é isso que o Monitor do Protheus faz quando você manda finalizar um processo. No momento que a aplicação perceber isso, ela finaliza o processo, com uma ocorrência de erro “Process terminated by Administrator” ou algo assim, gerando uma ocorrência de erro com o stack ou pilha de chamada de funções, para mostrar onde foi que o Protheus “percebeu” que o processo foi marcado para ser finalizado.

Nesta situação, quando o Programa AdvPL está em LOOP e não está interagindo com a Interface, a cada 10 segundos o Protheus Server verifica se o SmartClient ainda está lá, aguardando pela resposta. Caso o Server perceba que o SmartCient caiu, ou a conexão de rede foi interrompida, ele finaliza o processo atual com a ocorrência de erro fatal em AdvPL “Remote Connection BROKEN”.

Latência alta de rede entre Protheus e SmartClient

Normalmente quando isso acontece, o processo dá a impressão de ter “travado”, porém em alguns segundos a tela que deveria aparecer simplesmente “aparece”. Imagine que o programa em AdvPL está desenhando uma nova caixa de diálogo, com muitos componentes, e durante a montagem da tela o programa pede ao SmartClient coordenadas de tela e informações sobre as dimensões dos componentes em fase de montagem. Uma latência de rede momentânea de 500 ms (milissegundos) pode fazer uma tela que, durante sua montagem, faça 10 requisições ao SmartClient, demorar quase 5 segundos para ser finalizada. Num caso como esse, o monitor do Protheus mostra um numero de instruções por segundo perto de zero, e o número de instruções total sobe bem devagar.

Instrução em execução no Banco de Dados

Imaginem o cenário, onde a aplicação AdvPL monta uma Query dinâmica, porém devido a um erro de lógica ou validação de parâmetros, a Query fez um produto cartesiano da tabela, ou um INNER JOIN sem WHERE …. ou uma daquelas queries rebuscadas, que fazem múltiplas buscas em uma tabela enorme, usando um ou mais campos que não possuem um índice para o Banco de Dados otimizar a busca, e o Banco de Dados precisa fazer FULL SCAN (ler a tabela inteira) para retornar os dados solicitados.

Quando a aplicação AdvPL submeter a Query ao DBAccess, e este por sua vez submeter a query ao Banco de Dados, o AdvPL espera o retorno da API do DBAccess, e o DBAccess por sua vez está esperando o Banco de Dados. Isso também pode acontecer, por exemplo, com a chamada de uma Stored Procedure de processamento, quando parametrizada para rodar sobre grandes volumes de dados, ou mesmo falta de optimização de índices no banco para roda as queries submetidas por dentro da Stored Procedure.

No monitor do Protheus, será mostrado que o número total de instruções não aumenta, e o número de instruções por segundo permanece em 0 (zero). Ao abrir o Monitor do DBAccess — última versão do Portal — nós temos duas novas colunas de monitoramento muito úteis para casos como esse: A coluna “IDLE” e a “RUNNING”. A coluna IDLE indica a quantos segundos esta conexão do DBAccess não recebeu nenhum pedido de dados do programa AdvPL que a abriu, e a coluna “RUNNING” mostra naquele instante se e qual a rotina do DBAccess que está sendo executada.

Com isso, se a Query está ainda rodado dendo do Banco de Dados, a coluna RUNNING deve mostrar a operação OP_QUERY. No caso de uma Stored Procedure, se eu não me engano é a operação OP_SPEXEC.

Uma das colunas do DBACCESS Monitor — se eu não me engano DBSID ou apenas SID — mostra um identificador da conexão do DBAccess junto ao Banco de Dados. Esse identificador permite o DBA ou o Administrador do Ambiente a abrir uma conexão diretamente com o Banco de Dados, usando uma ferramenta de monitoramento nativa do Banco, e associar uma conexão mostrada pelo Monitor do SGDB com uma conexão do DBAccess.

Em um caso como esse, não adianta tentar matar a conexão do DBAccess com o Banco de Dados pelo Monitor do DBAccess, vai cair no mesmo problema do Protheus: Enquanto o DBAccess não receber um retorno da API do Banco, nada acontece … Mesma coisa derrubar com o Protheus Monitor … mesmo que você use a opção “derrubar imediatamente”, o SmartClient pode ser finalizado na hora, ao perceber que a conexão dele com o Protheus foi encerrada do lado do servidor, mas o programa AdvPL ainda vai estar esperando um retorno do DBAccess.

Neste caso, a última alternativa — antes de derrubar o serviço do Protheus e do DBAccess — é pegar o número ou identificador da conexão com o Banco de Dados, e usando uma ferramenta administrativa do Banco de Dados, pedir para o SGDB encerrar esse processo. Assim que isso foi feito, o SGDB retorna um erro de “Processo Interrompido” para o DBAccess, que por sua vez retorna este erro ao programa AdvPL.

Instrução em execução no SmartCient

Ao clicar naquele botão, o programa AdvPL em execução no Protheus Server pediu para o SmartClient abrir, por exemplo, uma URL a partir da estação onde o SmartClient está sedo executado — função HTTPCGet() — e o endereço solicitado está congestionado de requisições e coloca a sua na fila … Esta função tem um time-out de 120 segundos por default, o que pode “brecar o sistema” onde por 2 minutos.

Em um caso como esse ou similar, onde o Application Server está esperando por alguma coisa do SmartClient, quando você derruba (chuta) o SmartClient, finalizando o processo, o Protheus Server identifica que a interface de rede entre eles foi interrompida, e finaliza o programa AdvPL em execução com aquela ocorrência “Erro de Sincronismo”.

Conexão parcialmente fechada

Este é um cenário bem ingrato … você abriu um SmartClient, e iniciou aquele relatório que demora pelo menos uma hora … Você manda imprimir no SPOOL, e vai tomar um café. O programa que emite o relatório atualiza de vez em quando uma régua de processamento na tela do SmartClient. Passou uma hora, e a régua nem se mexeu … Você vai no Monitor do Protheus … e não encontra esse usuário. Vai no DBAccess, e também não acha nada … mas o SmartClient está ali, aberto, e a régua parada … e você não consegue nem clicar no botão cancelar … o que aconteceu?

Um caso como esse pode indicar uma conexão de rede parcialmente fechada. Durante o processamento do relatório, houve uma falha na rede, porém o encerramento da conexão TCP somente foi percebido pelo Protheus Server, quando ele foi atualizar a régua. Já o SmartClient, que fica somente esperando o Protheus pedir alguma coisa quando o controle de interface não está com ele, e existe um processamento no Protheus Server em andamento, caso a conexão TCP do lado do SmartClient não acuse o erro ou fechamento da outra ponta, o SmartClient fica esperando pra sempre um retorno que nunca vai chegar.

Cenários como esse podem ser contornados com a utilização de um aplicativo fornecido pela TOTVS para o Protheus Server chamado BROKER — ele serve de ponte e proxy reverso para as conexões do SmartClient para o(s) Protheus Server, inclusive para fazer balanceamento de carga. Ele entra na frente das conexões, tanto no SmartClient como no Application Server, e mantem uma conversa “constante” entre as pontas, sendo capaz de detectar com maior precisão quando uma das pontas foi desconectada,  e inclusive pode conseguir reconectar uma conexão encerrada devido a eventual e momentânea instabilidade na rede, sem que nenhuma das pontas (APPServer ou SmartClient) perceba o que aconteceu.

Ocorrências críticas

Um cenário difícil, mas plausível. Uma aplicação AdvPL reproduz um erro no Protheus Server, mostra uma caixa de diálogo com detalhes do erro no SmartClient, e quando você clica no “Ok” para fechar a janela, ela fecha. Então, você vai no DBAccess, e a conexão está lá .. e no license Server também … mas no Monitor do Protheus, esse usuário “sumiu”, e você tem certeza que ninguém usou aquele recurso de “derrubar imediatamente” aquele processo. O que pode ter acontecido?

Ao consultar o log de console do Protheus Server (console.log), você encontra o registro do Erro Advpl, logo depois uma mensagem parecida com “Critical Error”, seguida por “Falha no Delete da Thread” ou similar. Isto significa que, durante a descarga do ambiente, programas e recursos, ocorreu um erro critico na aplicação AdvPL, como pr exemplo invasão de memória, justamente enquanto aquele contexto de execução estava sendo limpo — executando os destrutores internos do Protheus. Se isso acontecer, uma parte dos recursos que seu processo continuam ativos neste processo mas o processo parcialmente não existe, somente o próprio processo consegue finalizar-se de modo elegante. Então, o processo some da lista de monitoramento, mas cai na malha dos processos com falha no destrutor.

Ocorrências desta natureza devem ser reportadas para a TOTVS, para a melhoria contínua do software. Normalmente a utilização de uma build DEBUG nestes casos ajuda a gerar um arquivo de CORE DUMP no momento que uma ocorrência crítica aconteça, gerando postas sólidas do que pode ter acontecido.

Conclusão

Espeto que estas poucas linhas ajudem aos analistas que procuram fantasmas nos ambientes do Protheus pelo mundo afora !!

Desejo novamente a todos TERABYTES DE SUCESSO !!! 

 

Protheus como Servidor de FTP

Introdução

Quando eu comentei um pouco sobre as capacidades do Servidor de Aplicação Protheus Server, em um post mais antigo, eu mencionei que ele não apenas servia a conexões do SmartClient para rodar aplicações AdvPL, mas também que ele poderia ser um servidor de HTTP, com páginas estáticas e dinâmicas — usando AdvPL ASP — bem como TELNET e FTP. No post de hoje, vamos explorar o que a gente puder sobre como usar um Protheus Server como servidor de FTP.

Configuração Mínima

Imagine que você quer usar um Protheus Server como um FTP Server, com acesso anônimo — sem criticar usuário e senha — e apenas disponibilizar uma estrutura de pastas para Download. Neste caso, a configuração mínima para este serviço, seria acrescentar no arquivo de configuração do Protheus (appserver.ini) a seção [FTP], com as seguinte chaves:

[ftp]
Enable=1
Port=21
Path=c:\Protheus12LG\EnvLight\ftp
CanAcceptAnonymous=1

Especificamos a porta padrão (21), o acesso anônimo habilitado, e o path raiz do FTP, a partir do qual as conexões terão acesso de Download. Dentro da pasta configurada em “path”, eu coloquei um arquivo chamado leiame.txt, vamos ver este acesso através de um cliente FTP nativo do Windows, usando o comando “ftp” em linha de comando.

C:\Users\siga0>ftp -A localhost
Connected to NOTE-JULIOW-SSD.
220 Connected to FTP server
331 Anonymous access allowed
502 Command not implemented
331 Anonymous access allowed, send email name as PASS
220 Logon successful
230 Welcome to Application Server FTP!
Anonymous login succeeded for siga0@NOTE-JULIOW-SSD
ftp> dir
250 PORT command successful
150 Opening ASCII mode data connection
-r-xr-x--- 1 owner group 39 Nov 04 20:10 leiame.txt
226 Transfer Complete
ftp: 74 bytes received in 0.00Seconds 37.00Kbytes/sec.
ftp> ls
250 PORT command successful
150 Opening ASCII mode data connection
leiame.txt
226 Transfer Complete
ftp: 15 bytes received in 0.00Seconds 15.00Kbytes/sec.
ftp>

Através do parâmetro “-A” na linha de comando, informamos ao cliente FTP que o Login deverá ser anônimo. Caso este parâmetro não seja especificado, você deve entrar manualmente com o usuário “anonymous“. Uma senha deve ser informada, mas não será validada — pode ser qualquer coisa, inclusive “anonymous”.

Após feito o login, executamos os comandos “ls” e “dir” para recuperar a lista de arquivos e pastas disponíveis para download. Vamos então fazer o download do arquivo “leiame.txt”:

ftp> get leiame.txt
250 PORT command successful
150 RETR command started
226 Transfer Complete
ftp: 39 bytes received in 0.00Seconds 39000.00Kbytes/sec.
ftp>

De dentro do FTP Client do Windows, podemos executar um comando do sistema operacional, prefixando ele com o sinal de exclamação. Por exemplo, para verificarmos  o conteúdo do arquivo na pasta local após o Download, vamos executar o comando “type”.

ftp> !type leiame.txt
Exemplo de Configuraτπo Mφnima de FTP
ftp>

No caso, o texto do arquivo justamente é “Exemplo de Configuração Mínima de FTP”. Porém, como a página de código do Prompt de Comando está com o CodePage 437 (CodePage original do IBM-PC, também conhecido por OEM-US, CP437 ou DOS Latin US), a acentuação é mostrada com outros caracteres. Para ver o arquivo da forma correta, ele pode ser aberto pelo NOTEPAD ou qualquer outro editor de textos, OU você deve digitar no Prompt de Comando a instrução abaixo, antes de abrir o cliente FTP:

mode con cp select=1252

Com isso, o seu Prompt de Comando vai usar o CodePage do Windows, CP1252, que também é o CodePage usado pelo Protheus. Para ver a lista de instruções implementadas na camada interna do FTP Server, use o comando remotehelp

ftp> remotehelp
214-The following commands are implemented
USER PASS ACCT QUIT PORT RETR
STOR DELE RNFR PWD CWD CDUP
MKD RMD NOOP TYPE MODE STRU
LIST HELP
214 HELP command successful
ftp>

Caso você tente fazer um upload no FTP nesta conexão, a operação será negada.

ftp> put upload.txt
250 PORT command successful
550 Access is denied
ftp>

Usando outros clientes FTP

Normalmente basta desligar o “Passive Mode” na configuração do programa que você usa como Cliente de FTP (SCP, WINSCP, etc.) que a conexão e operações são realizadas sem maiores problemas.

Implementando mais controles

Na configuração mínima, o FTP está totalmente aberto para download de qualquer arquivo colocado a partir da pasta configurada na chave PATH, para qualquer cliente que conecte usando a identificação “anonymous” — ou seja, sem autenticação alguma. No máximo, usando por exemplo um recurso externo, como um Firewall, você pode permitir por exemplo apenas receber conexões FTP na porta 21 a partir de um ou mais endereços de rede, e apenas isso.

Para atender a necessidade de permitir ou restringir operações por usuário, existe a necessidade de desligar o acesso anônimo, configurar algumas chaves adicionais na seção [FTP], e criar algumas funções AdvPL no repositório para serem acionadas por estas chaves. Vamos direto para o exemplo completo:

[ftp]
Enable=1
Port=21
Path=c:\Protheus12LG\EnvLight\ftp
RPCEnv=envlight
CheckPassword=U_FTPPASS
GetUserPath=U_FTPPATH
CheckUserOper=U_FTPOPER

Primeiramente, removemos o acesso anônimo. Então, criamos uma chave chamada RPCENV, onde colocamos o nome  do environment (ambiente) existente neste Protheus Server, responsável por executar as funções AdvPL que serão colocadas para validar algumas operações do FTP.

Configuração CHECKPASSWORD

Quando um usuário conectar no FTP e informar o usuário e senha, será chamada a função U_FTPPASS(), que receberá como parâmetros o usuário e senha informados pelo cliente de FTP. Se esta função retornar .T., o Protheus Server responde ao cliente de FTP que o login foi aceito, caso contrário responde uma mensagem de erro e nega o acesso. Vejamos o exemplo abaixo:

User Function FTPPass(cUser,cPass)
cUser := lower(cUser)
if ( cUser == "root" )
  if( cPass == "root" )
    Return .T.
  Endif
Endif
Return .F. 

Neste exemplo, permitimos apenas um usuário chamado “root”, com a senha “manager”  a entrar no FTP.

Configuração GETUSERPATH

Imagine que, eu quero fornecer, por exemplo, uma pasta raiz de FTP diferenciada para alguns usuários. Para isso, eu crio uma função AdvPL — no nosso exemplo, USER FUNCTION FTPPATH(), que recebe como parâmetros o usuário e senha informados no login. A função deve retornar um PATH completo no servidor onde está sendo executado o Protheus Server, e esta pasta será o diretório raiz de FTP. Vamos ao exemplo:

User Function FTPPath(cUser,cPass)
cUser := lower(cUser)
If cUser == "siga0984"
  return "C:\Protheus12LG\ftp"
Endif
Return "C:\Protheus12LG\ftp\anonymous"

Neste caso, quando o usuário de FTP for “siga0984“, ele têm acesso à pasta raiz do FTP, quando qualquer outro usuário somente terá acesso a partir da pasta “anonymous”.

Configuração CHECKUSEROPER

Caso você queira permitir UPLOAD de arquivos no FTP, ou outras operações que modifiquem conteúdo, como apagar arquivo, criar ou apagar uma pasta, é necessário implementar uma função AdvPL para ser chamada pelo Protheus Server para autorizar estas operações, usando a configuração CheckUserOper — no nosso exemplo, vamos implementar a função U_FTPOPER(). Ela recebe três parâmetros: O usuário de login no FTP Server, a senha utilizada, e o comando enviado pelo Cliente do FTP.

Apenas alguns comandos são desviados para esta função, por exemplo STOR <arquivo>, DELE <arquivo>, MKD <pasta>,  RMD <pasta>, e dois comandos não implementados para renomear arquivo (RNFR e RNTO).

  • STOR = Upload de arquivo
  • DELE = Apagar arquivo
  • MKD = Criar pasta 
  • RMD = Remover pasta
  • RNFR <arquivo1> e RNTO <arquivo2> — Renomar arquivo1 para arquivo2

Caso a função AdvPL retorne .T., a operação é autorizada. Caso contrário, negada. Vamos ao nosso exemplo:

User Function FTPOPER(cUser,cPass,cOper)
Local cCmd
cUser := lower(cUser)
If cUser == 'root'
  cCmd := left(cOper,4)
  If cCmd $ "STOR,DELE"
    Return .T.
  Endif
Endif
Return .F.

No exemplo acima, permitimos apenas ao usuário “root” a possibilidade de fazer upload ou mesmo de apagar um arquivo remotamente.

Resultados dos Testes

Os testes realizados mostraram que alguns clientes de FTP, por exemplo o WINSCP, usou uma sintaxe para a troca de pasta (comando CWD) que o FTP Server do Protheus não entendeu, mas funcionou adequadamente com o cliente FTP nativo do Windows em linha de comando, e um cliente de FTP do Altap(r) Salamander.

Mesmo com as implementações em AdvPL, o objetivo de ter um servidor nativo de FTP no servidor Protheus é atender a necessidade de integrações entre sistemas, normalmente em ambientes restritos — ou fechados. Ele não oferece logs de utilização nativos, não permite interceptar outros comandos para implementar por exemplo restrição de acesso de usuário para uma pasta ou arquivo, etc.

Devido a questões ligadas a implementação do FTP em múltiplas plataformas, é recomendado usar os nomes de arquivos sem espaços em branco, sem acentuação, e com letras minúsculas, e utilizar sub-pastas se e somente se realmente necessário. Qualquer demanda maior, que exija mais controles, como um FTP publicado na Internet para clientes e parceiros ter acesso a múltiplos arquivos, eu pessoalmente recomendo a utilização de uma aplicação especializada em ser Servidor de FTP, que vai lhe oferecer nativamente muito mais controles do que o Protheus Server como FTP Server.

Conclusão

Para cada tamanho de problema, existe uma solução adequada. O Protheus como servidor de FTP não foi criado para competir com um FTP Server de mercado, mas apenas para ter uma alternativa simples e nativa para integração entre sistemas, onde não são necessários níveis muito avançados de controle. Porém, para o que ele se propõe, ele dá conta do recado.

Em um próximo post, vamos explorar a classe client de FTP do Protheus Server —  chamada TFTPCLIENT() — para conectar e realizar operações de Cliente de FTP conectando-se em um FTP Server configurado também no Protheus.

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

Referências