ZLIB Framework – Parte 01

Introdução

Vamos ver um pouco sobre Bibliotecas de Funções e Framework, com destaque para as funcionalidades em implementação no projeto ZLIB.

Bibliotecas e Frameworks

Com as funções básicas da linguagem, conseguimos criar qualquer programa. Alguns programas podem dar mais trabalho que outros, tudo depende de quantas funcionalidades serão implementadas. Porém, quando você precisa implementar muitas funcionalidades parecidas, é mais eficiente isolar o código comum em classes ou funções parametrizáveis, para não ter que escrever tudo de novo ou copiar-e-colar, replicando código desnecessariamente. Neste ponto, começa o nascimento de uma Biblioteca de funções.

Na ciência da computaçãobiblioteca é uma coleção de subprogramas utilizados no desenvolvimento de software. Bibliotecas contém código e dados auxiliares, que provém serviços a programas independentes, o que permite o compartilhamento e a alteração de código e dados de forma modular. Alguns executáveis são tanto programas independentes quanto bibliotecas, mas a maioria das bibliotecas não são executáveis.

Quando falamos em Framework, não apenas estamos usando funções genéricas de uma biblioteca, mas sim uma abstração de nível mais alto, que impõe um fluxo de controle na aplicação.

Um framework em desenvolvimento de software, é uma abstração que une códigos comuns entre vários projetos de software provendo uma funcionalidade genérica. Um framework pode atingir uma funcionalidade específica, por configuração, durante a programação de uma aplicação. Ao contrário das bibliotecas, é o framework quem dita o fluxo de controle da aplicação, chamado de Inversão de Controle.[1]

Projeto ZLIB

A ideia — necessidade — de uma LIB (Biblioteca) de componentes surgiu com os posts da série do CRUD em AdvPL, que acabou virando uma Agenda de Contatos, feita originalmente atrelada a interface do SmartClient, e depois implementada em uma interface WEB/HTTP.

Muito daquele código é comum a aplicações de mesma funcionalidade — cadastro simples. Inclusão, Alteração, Exclusão, Consulta ordenada, consulta por filtro. Outras funcionalidades, como exibição e cadastro de imagem, envio de email e mapa do endereço não necessariamente são usadas em todos os cadastros, mas podem ser colocadas em componentes de uma biblioteca para reaproveitamento.

A ideia da ZLIB é ser uma Biblioteca de Funções, que vai servir de base para construir um Framework. Ela já está versionada no GITHUB, mas ainda em desenvolvimento e com pouca (nenhuma) documentação, e como os componentes ainda estão nascendo, muitas alterações drásticas estão sendo feitas a cada atualização.

Orientação a Objetos e Abstração

Estas são duas chaves importantes no reaproveitamento de código e desenvolvimento modular. A orientação a objetos nos permite criar classes com uma finalidade (abstração) e implementar para múltiplos cenários ou recursos.

Por exemplo, as classes implementadas para acesso a arquivos DBF e arquivos em memória. Ambas possuem a mesma declaração de métodos para implementar as suas funcionalidades. Logo, o mesmo programa que insere um registro em uma tabela da classe ZDBFFILE pode realizar a mesma operação usando um objeto da ZMEMFILE.

Uma classe de geração de LOG de operação ou execução não precisa saber onde o log será gravado, ou mesmo conhecer a interface de gravação. Ela pode receber como parâmetro um objeto de uma classe de gravação de LOG. Ele pode ser de uma classe que grave os registros emitidos de log em um arquivo TXT, ou em um banco de dados, ou ainda seja um encapsulamento de uma interface “client” de log, que envia os dados gerados para serem gravados remotamente por um Log Server.

Criação de Componentes

Um dos primeiros mandamentos da criação de componentes é : A CRIAÇÃO DE QUALQUER COMPONENTE DEVE SER MOTIVADA PELA NECESSIDADE. Criar componentes adicionais ou agregar funcionalidades demais a um componente só por que vai ser “legal” só engorda código. Limite-se a uma funcionalidade por classe, e coloque nela o que realmente é comum a todos. Exceções são tratadas na implementação, a abstração é genérica.

Quando aos níveis de implementação — ou camadas — normalmente os componentes de alto nível são construídos para usar os de mais baixo nível. Na prática eles são construídos para usar todas as implementações feitas sobre uma abstração. Por exemplo, um componente de CRUD feito para usar a abstração ZISAMFILE pode usar qualquer implementação feita sobre ela, como a ZDBFFILE, ZTOPFILE, ZMEMFILE…

Como a implementação está por baixo da abstração, eu posso por exemplo criar uma abstração de exportação de arquivo, e implementar uma exportação para cada formato, a mesma coisa para importação.

Objetivo Final

Criar um conjunto de funções e funcionalidades que, permitam escrever programas, funções e rotinas, separando totalmente o processamento da interface, focando em SOA utilizando micro-serviços, filas e controladores, com foco em desempenho, escalabilidade, resiliência e alta disponibilidade.

Conclusão

Por hora, a primeira missão das funções em desenvolvimento é permitir a reescrita do programa de Agenda para SmartClient, usando componentes destacados, que permitam um elevado índice de reaproveitamento de código, e uma forma de declarar e executar as validações e procedimentos de cada operação que torne a codificação mais fácil e rápida, usando uma abordagem que permita aproveitar o CORE de cada componente em integrações encapsuladas por APIs (RPC Advpl, REST, SOAP) para serem consumidas por interfaces criadas em AdvPL ou qualquer outra linguagem ou plataforma.

Referências

Arquivos em Memória – Classe ZMEMFILE

Introdução

Nos posts anteriores, acompanhamos a criação de uma classe de acesso a dados ISAM — chamada de ZDBFTABLE, renomeada para ZDBFFILE — , feita para leitura e manutenção de arquivos no formato DBF em AdvPL, sem dependência de nenhum Driver. Agora, tomando esta classe como base da implementação, nasceu a classe ZMEMFILE.

Classe ZMEMFILE

O lindo da orientação a objetos é o reaproveitamento de código. Como eu não comecei a implementação com uma classe abstrata, e não pretendia criar uma agora, a classe ZMEMFILE nasceu de um “Clone” da classe ZDBFFILE. A diferença é que, ao invés de eu endereçar um handler de arquivo em disco para ler e gravar dados, eu criei na classe uma propriedade chamada ::aFileData, que é um array multi-dimensional com as colunas da tabela, e uma coluna interna a mais — para indicar se o registro foi marcado para deleção ou não.

O meu “RECNO” passa a ser o próprio elemento do array. Cada inserção de novo registro é feita no final do array, e a deleção apenas habilita um flag na ultima coluna do array. Os demais mecanismos são os mesmos, o registro atual lido em memória é uma cópia do original em um array separado, a atualização de valores idem, e os dados da tabela estão associados ao objeto da tabela, visível apenas pelo processo atual, enquanto o objeto não foi destruído ou a tabela for fechada.

Todos os demais métodos da classe que não acessavam fisicamente o arquivo, simplesmente não foram alterados. Estes métodos são os candidatos para uma futura refatoração, criando uma classe superior com estes métodos, fazendo as classes ZDBFFILE e ZMEMFILE herdarem esta classe base, e removendo as duplicidades desnecessárias da implementação.

Aproveitamento da classe ZMEMINDEX

Como a classe que cria o índice em memória não acessa diretamente os dados de nenhum arquivo, mas faz as leituras, criação de índice e demais operações usando os métodos da classe ZDBFFILE, eu praticamente não precisei mexer em nenhuma linha da ZMEMINDEX para usá-la com a ZMEMFILE. 

Isso me deixou simplesmente radiante. A implementação de filtro foi clonada, a implementação de bisca indexada e manutenção de índices também clonada, uma vez que eu reimplementei os métodos que efetivamente acessavam o disco para acessar um array da própria classe, o fonte já funcionava.

Fonte de Testes

Vamos ver o fonte abaixo, chamado de CriaMem.PRW

#include "protheus.ch"

USER Function CriaMEM()
Local cFile := 'memfile.dbf'
Local oDbf
Local aStru := {}

SET DATE BRITISH
SET CENTURY ON 
SET EPOCH TO 1950

// Define a estrutura 
aadd(aStru,{"CPOC","C",10,0})
aadd(aStru,{"CPOM","M",10,0})

// Cria o objeto da tabela 
oDbf := ZMEMFILE():New(cFile)

// Cria a tabela em si 
oDbf:Create(aStru)

// Abre em modo de escrita 
If !oDbf:Open(.T.,.T.)
	UserException( oDBF:GetErrorStr() )
Endif

// Insere um registro
oDBF:Insert()
oDBF:Fieldput(1,'Laranja')
oDBF:Fieldput(2,'0000000001')
oDBF:Update()

// Insere mais um registro 
oDBF:Insert()
oDBF:Fieldput(1,'Banana')
oDBF:Fieldput(2,'0000000002')
oDBF:Update()

// Insere um terceiro registro 
oDBF:Insert()
oDBF:Fieldput(1,'Abacate')
oDBF:Fieldput(2,'0000000003')
oDBF:Update()

conout("Mostrando 3 registros")
oDBF:GoTop()
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Agora cria um indice
conout("Criando indice por CPOC")
oDBF:CreateIndex("CPOC")

// Mostra os dados da tabela novamente, agora ordenados
// A criacao de um indice já o torna ativo, e reposiciona 
// a tabela no primeiro registro 
While !oDBF:Eof()
	// Mostra o registro atual
	ShowRecord(oDBF)
	oDBF:Skip()
Enddo

// Fecha a tabela
// -- Os dados sao eliminados da memoria 
oDBF:Close()

// Limpa / Libera o Objeto
FreeObj(oDBF)

Return

// Função feita para mostrar o conteudo do registro atual 
STATIC Function ShowRecord(oDBF)
Local nI
Local aStru := oDBF:GetStruct()
conout(replicate('-' ,79))
conout("RECNO() ...... " + cValToChar(oDBF:Recno()) +" | " + "DELETED() .... "+cValToChar(oDBF:Deleted()) )
conout("BOF() ........ " + cValToChar(oDBF:Bof())   +" | " + "EOF() ........ "+cValToChar(oDBF:Eof()) )
conout("Index ........ " + "("+cValToChar(oDBF:IndexOrd())+") "+oDBF:IndexKey())
conout("")
For nI := 1 to len(aStru)
	conout("Fld #"+padr(cValToChar(nI),3)+" | "+ ;
	  aStru[nI][1]+" ("+aStru[nI][2]+") => ["+cValToChar(oDBF:Fieldget(nI))+"]" )
Next
conout("")
Return

Em poucas palavras, o fonte de exemplo cria a tabela, insere três registros, mostra no console, cria um índice pelo campo “CPOC”, e mostra novamente os dados ordenados.

Sabe o que você precisa fazer para isso rodar com um DBF ? Apenas troque a seguinte linha:

// Troque a linha abaixo 
oDbf := ZMEMFILE():New(cFile)

// Para esta aqui:
oDbf := ZDBFFILE():New(cFile)

E está feito. Salve o fonte, compile novamente e teste. O arquivo chamado “memfile.dfb” será criado no disco. Recomendo baixar a pasta inteira do GirHub (Blog), e criar um projeto com os seguintes fontes:

zDBFFile.prw
zDbfMemIndex.prw
zDBTFile.prw
zFPTFile.prw
zLibDateTime.prw
zLibDec2Hex.prw
zLibNBin.prw
zMEMFile.prw

Na próxima refatoração, a classe ZDBFMEMINDEX será renomeada para ZMEMINDEX — afinal ela não serve apenas para DBF 😀

Por que não usar um Array “direto” ?

A-há !! A pergunta de meio milhão de dinheiros …risos… Sim, armazenar um resultado temporário de qualquer coisa em um array em AdvPL pode ser feito de forma direta, sem dúvida. Se o seu uso de dados em Array é simples, o array é pequeno e as buscas são ridículas, e a manutenção de elementos praticamente inexistente, então você não precisa usar uma classe para emular um arquivo em memória. Escreva seu fonte criando um array direto e seja feliz.

Agora, se você vai ter um número maior de elementos, precisa de uma busca ou navegação ordenada — HASH faz busca, mas não ordena — e já tem um código que usa o arquivo em disco, como um container temporário, mas ele não vai acabar com a memória do Protheus Server se ele for usado em memória … pronto, você tem uma implementação virtual de arquivo para pintar e bordar.

Evolução natural de funcionalidades

Conforme a implementação vai sendo utilizada, naturalmente sentimos a necessidade de algumas melhorias, alguns métodos ou mesmo novas classes facilitadoras. Por exemplo, copiar dados de um arquivo em disco para a memória. É simples montar, basta instanciar o arquivo em disco em um objeto, pegar a estrutura, criar o objeto em memória, fazer um loop lendo  o primeiro arquivo e inserindo os dados no segundo arquivo.

Porém, se cada vez que você precisar fazer isso, você replicar a implementação, dá-lhe código duplicado. Nada como implementar um método CreateFrom(). Você cria o objeto do arquivo no disco, onde estão os dados, cria o objeto do arquivo em memória, então chama o método CreateFrom() do arquivo em memória, passando como parâmetro o objeto do arquivo em disco. O método CreateFrom() ainda não existe, mas é o próximo da fila… risos…

Internamente este método já vai fazer o que tem que ser feito. Quer algo mais elegante que isso? Seu arquivo temporário em memória não precisa de todos os dados do arquivo em disco, porém apenas os registros que atendam uma determinada condição. Basta setar um filtro no arquivo de origem, o método CreateFrom() vai copiar apenas os registros logicamente visíveis, que atendam a condição de filtro.

Conclusão

Por hora, a conclusão óbvia é que, embora inicialmente pareça um pouco mais difícil mudar seu mind-set para pensar Orientado a Objetos, adotar este paradigma da forma consciente e adequada só têm benefícios.

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

Referências

PROGRAMAÇÃO ORIENTADA A OBJETOS. In: WIKIPÉDIA, a enciclopédia livre. Flórida: Wikimedia Foundation, 2018. Disponível em: <https://pt.wikipedia.org/w/index.php?title=Programa%C3%A7%C3%A3o_orientada_a_objetos&oldid=53496356>. Acesso em: 2 nov. 2018.

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

 

 

Boas Práticas de Programação – Código Simples, Resultados Rápidos

Introdução

Este tópico está entre “Dicas Valiosas” e “Boas Práticas de Programação”, de qualquer modo aborda um exemplo de código, ainda em desenvolvimento por um colega que está iniciando no “Mundo do AdvPL”. Partindo deste fonte de exemplo, vamos analisar o que o fonte faz, como faz, e inclusive avaliar algumas opções de escrever um algoritmo que faça a mesma coisa, usando funções diferentes ou escritos de uma outra forma, e se existe algum ganho ou perda, direto ou indireto ao optarmos por uma ou outra forma de resolução.

O Fonte em AdvPL – Primeira versão

#INCLUDE "PROTHEUS.CH"
#INCLUDE "TOTVS.CH"
#INCLUDE "FILEIO.CH"

USER FUNCTION TESTE()
  
        local nArquivo
        local i
        local nNumero
        local nNum := 7 

        msgAlert("Início.")

        nArquivo := fcreate("C:\devstudio\Melhorias\src\arquivo.txt")

        if ferror() != 0
                msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror()))
                return
        endif 
        for i:=1 to 500
                nNumero = randomize(100000,999999)
                if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente
                        fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10))
                else
                        fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10))
                Endif
        next    
        fclose(nArquivo)

        msgAlert("Fim.")
Return

Engenharia Reversa

Olhando o fonte linha por linha, podemos deduzir o que ele faz ao ser executado. Inicialmente, o fonte declara as variáveis utilizadas com escopo “Local”, mostra uma mensagem na tela do SmartClient informando que o programa está no “Início”, após pressionar OK ou o simplesmente fechar a janela a aplicação continua, criando um arquivo em um path específico na máquina onde está sendo executado o SmartClient –que pode ser a mesma máquina onde o SmartClient pode estar sendo executado, ou uma outra máquina conectando neste Protheus Server via REDE TCP/IP.  — Arquivo “C:\devstudio\Melhorias\src\arquivo.txt””.

A aplicação usa fCreate() para criar o arquivo, verifica se houve falha na criação verificando o retorno da função fError(), faz um loop de 500 iterações,  sorteia um número entre 100000 e 999999, e escreve no arquivo o número sorteado usando fWrite(), e antes de escrever determina se o número sorteado é múltiplo de 7 ou não, usando a função Mod().

Primeira Revisão

Logo nas primeiras linhas do arquivo, foram usados dois includes: “protheus.ch” e “totvs.ch”. Internamente, ambos são o mesmo arquivo. Na verdade, o include totvs.ch inclui internamente o arquivo protheus.ch. Foi criado um include com o nome de “totvs.ch” apenas para padronização de código. Logo, tanto faz qual deles for usado, precisamos somente de um deles.

O include fileio.ch possui algumas constantes que opcionalmente podem ser usadas pelas funções de baixo nível de arquivo, como a fCreate(), fOpen() e fSeek(). No caso do fonte acima, como nenhuma constante foi usada, fazer referência a este arquivo também é desnecessário.

Quanto a verificação de erro de criação de arquivo, normalmente verificamos se o resultado da função fCreate() é diferente de -1 (menos um), pois este é o valor retornado em caso de falha da função. Porém, não há nada de mais em usar a função fError() — desde que logo após a chamada da fCreate() — pois em caso de sucesso, fError() retorna 0 (zero).

Dento do laço for…next de 500 iterações, cada volta sorteia um número e verifica se ele é ou não múltiplo de 7. Quando queremos uma conversão de um valor numérico para string, sem espaços adicionais, podemos usar tranquilamente a expressão alltrim(str(nNumero)). De qualquer modo, no AdvPL temos uma função básica da linguagem que realiza exatamente este tipo de conversão, inclusive para outros tipos de dados de entrada, resultando em uma string — chama-se cValToChar(). A utilização dela neste cenário apenas economiza a chamada de funções — ao invés de duas, realizamos a conversão com apenas uma.

Na declaração de variáveis, foi usada corretamente a notação húngara — a primeira letra do nome da variável indica o seu tipo. Isto é “praxe” em AdvPL, ajuda bastante. E, outra coisa que ajuda é colocar nomes que tenham relação com o conteúdo. Olhando o fonte, sabemos que nNumero foi gerado por sorteio, e nNum é o número para verificar se o primeiro é múltiplo dele. Porém, ficaria mais “elegante” nomear nNum com, por exemplo “nDivTst” — divisor de teste.

Outros modos de fazer a mesma coisa

Existem duas formas de verificar se um número tem resto inteiro após uma divisão por outro. Podemos usar a função “mod()” ou o operador “%“, o resultado é o mesmo, ambos estão certos.

if ( mod(nNumero, nNum )  ==  0)
if ( ( nNumero % nNum ) == 0 )

 

Porém, existe também uma forma de redução de código, que eu particularmente acho feia, pois dificulta a legibilidade do código, e não há ganho NENHUM em desempenho, por exemplo:

if (mod(nNumero,nNum)) == 0 //dividendo e divisor respectivamente
   fWrite(nArquivo, "O numero < " + alltrim(str(nNumero)) + " > é divisível por " + alltrim(str(nNum)) + chr(13) + chr(10))
else
   fwrite(nArquivo, "O numero <" + alltrim(str(nNumero)) + "> não é divisível " + alltrim(str(nNum)) + chr(13) + chr(10))
Endif

O código acima poderia ser escrito de outra forma, com a mesma funcionalidade, porém com uma sintaxe totalmente diferente.

fWrite(nArquivo, ;
    "O numero <" + cValToChar(nNumero) + ">"+;
    IIF( mod(nNumero,nNum) == 0 , " é "," não é ") + ;
    "divisível por " + cValToChar(nNum) +chr(13)+chr(10) )

EXCEPCIONALMENTE NESSE EXEMPLO usando o recurso de quebrar linhas de código com ponto e vírgula no final, e tratando uma condição SIMPLES, com duas situações de retorno PEQUENO, escrever dessa forma não ficou tão “feio”. Usamos a função IIF() — ou “if em linha” — cujo comportamento é avaliar e retornar o segundo parâmetro informado caso a condição informada no primeiro argumento seja verdadeira, ou processar e retornar o terceiro parâmetro caso a condição informada seja falsa.

De qualquer modo, eu recomendo fortemente que esse tipo de redução seja usada com muita cautela, escrever um código todo rebuscado cheio de operadores e expressões “MegaZórdicas” não quer dizer que você é um bom programador. Escrever o IF — ELSE — ENDIF tradicional e colocar exatamente o que você quer fazer, mesmo que em alguns casos possa repetir um pouco de código, é preferível para dar LEGIBILIDADE ao código.

“Codifique sempre como se o cara que for dar manutenção seja um psicopata violento que sabe onde você mora.” – Martin Golding

“Qualquer um pode escrever um código que o computador entenda. Bons programadores escrevem códigos que os humanos entendam.” – Martin Fowler

 

Desempenho

Para realizar um teste de desempenho, eu removi as chamadas de MsgAlert(), e inseri uma contagem de tempo, declarando uma variável nTempo como “Local”, atribuindo nesta variável imediatamente antes da instrução “For” o retorno da função seconds(), e depois na instrução “Next”, eu mostro na tela uma MsgAlert() com o tempo que a função demorou para executar o loop de geração de números e de gravação em disco, com precisão de 3 casas decimais.  O fonte após a revisão ficou desta forma:

#INCLUDE "PROTHEUS.CH"

USER Function TESTE()
Local nArquivo , nNumero
Local nDiv := 7 
Local i, nTempo

nArquivo := fcreate("c:\Temp\arquivo.txt")
If ferror() != 0
   MsgAlert("ERRO GRAVANDO ARQUIVO, ERRO: " + cValToChar(ferror()))
   return
EndIf
nTempo := seconds()
For i:=1 to 5000
   nNumero = randomize(100000,999999)
   If (mod(nNumero,nDiv)) == 0
      fWrite(nArquivo, "O numero < " + cValToChar(nNumero) + " > é divisível por " + cValToChar(nDiv) + chr(13) + chr(10))
   Else
      fwrite(nArquivo, "O numero <" + cValToChar(nNumero) + "> não é divisível " + cValToChar(nDiv) + chr(13) + chr(10))
   EndIf
Next
MsgAlert("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.')
fclose(nArquivo)
Return

Para uma métrica de tempo mais apurada, 500 iterações era muito pouco. Aumentei para 5000 iterações, e no meu equipamento a execução deste código demora entre 500 e 600 milissegundos (0.5 a 0.6 segundos).

Pontos de Atenção

Após olhar o arquivo com as informações geradas, reparei em um comportamento interessante. Os números gerados randomicamente começavam em 100000 , mas nunca eram maiores que 132000. Após consultar a documentação da função Randomize() na TDN, reparei que ela têm uma observação sobre seu funcionamento.

A função Randomize(), trabalha com um intervalo interno de 32767 números, a partir do número inicial informado, inclusive se o número inicial for negativo.

Logo, embora eu possa especificar um número inicial e final, se a diferença entre ambos for maior que 32767, ela considera o limite final igual ao limite inicial + 32767 menos uma unidade, e não o limite informado. Neste caso, precisamos usar um outro algoritmo para chegar a um número tão grande —  veremos como contornar isto mais para frente, em um próximo post.

Pontos de Melhoria

Sempre têm alguma coisinha que podemos fazer para melhorar o código, direta ou indiretamente. No caso do programa de exemplo acima, por uma mera questão circunstancial o arquivo a ser criado e que receberá os dados a serem gravados foi passado para a função contendo o que chamados de “path completo”, desde a unidade de disco, diretório e nome do arquivo com extensão. A maioria das funções de baixo nível de arquivo do AdvPL entende que, se um arquivo possui a letra da unidade de disco informada, este arquivo deve ser criado e manipulado pelo SmartClient.

Isto significa que, quando o Servidor roda a instrução fCreate(), ele envia uma mensagem ao SmartClient para que ele crie o arquivo, não importa se o SmartClient e o Protheus Server estão rodando na mesma máquina. E, cada instrução fWrite() executada faz o Protheus Server enviar uma linha com as informações para o SmartClient acrescentar no arquivo.

Os tempos de execução deste programa no meu ambiente, inserindo 5 mil linhas, estava em torno de 0.5 a 0.6 segundos. Vamos criar dentro da pasta do ambiente atual em uso (RootPath especificado na configuração de ambiente do appserver.ini) uma pasta chamada “Temp”, e no fonte AdvPL, vamos passar a criar este arquivo a partir do Servidor, mais precisamente dentro da pasta “Temp”.

nArquivo := fcreate("c:\Temp\arquivo.txt")

Deve ser trocado para

nArquivo := fcreate("\Temp\arquivo.txt")

Agora, executamos o programa novamente, e … surpresa, o tempo de execução CAIU  para 0,170 a 0,200 segundos. Isso é quase três vezes mais rápido, e olhe que no meu ambiente o SmartClient e o Application Server estão no mesmo equipamento. Logo, este tempo a mais — que chamamos de “overhead” — envolve a aplicação montar a mensagem de gravação para o SmartClient, enviar pela rede, o SmartClient receber a mensagem, decodificar a mensagem, processar a mensagem e informar que a operação foi executada com sucesso. Esta diferença de desempenho é mais acentuada ainda quando o SmartClient e o Application Server estão realmente sendo executados em duas máquinas diferentes, com uma rede TCP entre elas.

É possível melhorar esse tempo no SmartClient ?

Sim, é possível sim, melhorar significativamente. Repare que cada iteração no fonte gera uma linha de aproximadamente 41 bytes quando o número não é divisível por 7, e 37 bytes quando é divisível. Um pacote único de rede TCP normalmente suporta até 1500 bytes. Logo, se ao invés de escrever 36 linhas de 41 bytes, uma para cada fWrite(), a gente acumulasse na memória essas 36 linhas em uma variável caractere, gerando uma string de aproximadamente 1476 bytes, e enviasse essa string na função fWrite(), podemos reduzir bem o tempo de transmissão de dados na rede, pois é mais rápido transmitir um pacote de 1476 bytes do que transmitir 36 pacotes de 41 bytes. Neste caso, o fonte ficaria assim:

USER Function TESTE()
Local nArquivo , nNumero
Local nNum := 7 
Local i, nTempo
Local nX
Local cTemp := ''

nArquivo := fcreate("c:\Temp\arquivo.txt")
If ferror() != 0
   msgalert ("ERRO GRAVANDO ARQUIVO, ERRO: " + str(ferror()))
   return
EndIf
nTempo := seconds()
For i:=1 to 5000
   nNumero = Randomize(100000,999999)
   If ( nNumero % nNum ) == 0
      cTemp += "O numero <" + cValToChar(nNumero) + "> é divisível por " + cValToChar(nNum) + chr(13) + chr(10)
   Else
      cTemp += "O numero <" + cValToChar(nNumero) + "> não é divisível por " + cValToChar(nNum) + chr(13) + chr(10)
   EndIf
   IF i % 36 == 0 
      fWrite(nArquivo,cTemp)
      cTemp := ''
   Endif
Next 
if !empty(cTemp)
   fWrite(nArquivo,cTemp)
Endif
conout("Tempo Total = "+str(seconds()-nTempo,12,3)+' s.')
fclose(nArquivo)
return

Conclusão

Para meu espanto, na máquina local (APPServer e SmartClient no mesmo equipamento), os tempos ficaram praticamente iguais, gravando o arquivo no path “c:\Temp” através do SmartClient, ou gravando dentro da pasta “\temp\” a partir do RootPath do ambiente no APPServer.

Espero que este post agregue conhecimento para quem está começando no AdvPL, e para quem já têm alguma experiência com a linguagem e com o ambiente. Afinal, conhecimento nunca é demais. Desejo a todos TERABYTES DE SUCESSO. 

Agradecimentos

Agradeço o colaborador Hussyvel Ribeiro, que me concedeu um fonte de testes para usar neste post. Hussyvel, seja bem vindo ao AdvPL !!! 😀 

Referências

 

 

CRUD em AdvPL – Parte 14

Introdução

No post anterior, criamos um programa para servir de “Menu” para a Agenda e outras funcionalidades a serem criadas pela aplicação. Porém, não foi colocada nenhuma proteção para a execução do programa — Controle de Acesso ou similar. Vamos ver como fazer isso de forma segura e elegante, e ver alguns parágrafos sobre Segurança da Informação.

Controle de Acesso

Quando pensamos em restringir o acesso a uma determinada informação, precisamos avaliar o quão importante é a informação em si, e o quão desastroso seria se outra pessoa — além de você — tivesse acesso a esta informação. Determinado este fator, e ele sendo considerado de alta importância, precisamos ver quais são as formas ou caminhos de acesso a informação que podem ser utilizadas, e não apenas como protegê-las, mas o custo disso.

Quando me refiro a custo, não necessariamente estou falando de dinheiro, mas em custo de implementação — em horas — e custo de acesso a informação — desempenho da aplicação ser diminuída em virtude das barreiras de segurança. Um outro fator também importante é “Quanto vai me custar se eu perder a informação ?!” — já este aspecto também é tratado na Segurança da Informação, pois mesmo que a informação não tenha valor para outra pessoa, para você ela deve ter.

E você está sujeito a perder seus dados de várias formas — desde um ataque intencional para prejudicar você ou a sua Empresa, até mesmo um incidente com um ou mais equipamentos … por exemplo, a sala dos servidores ficava no subsolo da empresa, e devido a uma enchente, a água alagou a sala até o teto … danificando permanentemente os computadores  e os discos de dados. Então você lembra que têm um BACKUP (cópia) dos dados, feito no dia anterior — que infelizmente estava em outro servidor, NA MESMA SALA 😐 Mas esse aspecto de segurança contra desastres a gente aborda em outro tópico … risos …

Usuário e Senha

Pensando inicialmente como um usuário da Agenda, e eu quero que somente pessoas que eu autorize tenham acesso para ler e mexer na minha Agenda, exigir na entrada do programa o fornecimento de um nome ou identificador do operador/usuário e uma senha já é um bom começo.

Se, por outro lado, eu quero definir que, um determinado usuário não pode ver ou alterar determinados contatos, ou não pode excluir ninguém da agenda, mas outro usuário pode, podem ser criados mais controles — como direitos de acesso e operação — que podem ser definidos por usuário ou ainda por grupos de usuários, e por operação. Quando trabalhamos com muitos usuários e direitos, torna-se vantajoso criar grupos de usuários e dar os direitos comuns ao grupo, e depois adicionar ou relacionar o usuário a um ou mais grupos. Por definição, um usuário que pertence a um grupo herda os direitos do grupo. Esta abordagem é muito comum em sistemas operacionais (Windows, Linux, Unix, etc.)

Restrições e/ou Liberações

Normalmente partimos de duas abordagens básicas: Restringir ou Liberar. Podemos partir de um sistema de segurança aberto — onde tudo é permitido — e inserir restrições — usuário pode tudo, exceto operações X e Y — ou partir de um sistema de segurança fechado — onde nada é permitido — e as operações ou recursos devem ser explicitamente liberados para um operador ter acesso as operações.

Ainda podemos trabalhar de forma mista, onde por exemplo as operações de um determinado recurso — cadastro de usuários, por exemplo — por padrão é bloqueado para todos, exigindo liberação para acesso, e o acesso aos dados da agenda — liberado por padrão — mas que, mediante a inserção de restrições, podem ter algumas operações ou mesmo o próprio acesso a rotina bloqueados.

Via de regra, um controle de acesso visa tratar a exceção. Logo, se a maioria dos operadores deve ter acesso a agenda, é mais fácil e custa menos inserir restrições quando e onde necessário. Por outro lado, se ter acesso a um determinado recurso é a exceção — via de regra a maioria não deveria ter acesso — então optamos por trabalhar com uma liberação.

Acesso externo (por fora da aplicação)

Eu posso não ter acesso ao programa de Agenda, porém o programa usa um Banco de Dados para armazenar as informações. Se o operador da máquina estiver usando o meu terminal, e o meu Banco de Dados tiver uma relação de confiança com o meu usuário local, eu abro a ferramenta de administração do Banco de Dados, e posso fazer o que eu quiser com os dados, desde consultar até excluir. Uma alternativa é criar o banco com um usuário diferente, com senha forte, e com autenticação explícita por usuário e senha.

Se eu tenho acesso ao computador onde a Agenda está instalada, mesmo que eu não tenha acesso ao Banco de Dados por dentro de uma ferramenta, eu posso tentar parar o serviço do Banco, copiar os arquivos de dados e logs do SGBD, e montar o banco em outro computador. A alternativa para proteger um acesso aos dados mediante cópia é habilitar uma criptografia dos dados no próprio Banco de Dados. Assim, mesmo que alguém consiga copiar o Banco inteiro, sem a chave de segurança ninguém vai entender o que está gravado ali. Alternativas como essa exigem planejamento e controle, pois se você mesmo perder a chave, nem você mais acessa os dados.

Se o seu equipamento está conectado na Internet, e você não tomou medidas de segurança, desde o procedimento de não instalar qualquer coisa de qualquer lugar no equipamento, e não bloqueou alguns acessos e serviços normalmente visados e com brechas conhecidas de ataque, ou colocou uma senha fraca em um usuário com direitos administrativos no seu equipamento, um acesso remoto na máquina tem praticamente o mesmo efeito como se o intruso estivesse realmente na frente do equipamento — quando ele pode estar a 2 quadras ou 50 mil quilômetros de distância.

RANSOMWARE

Um adendo especial, sobre um novo tipo de ataque, que já fez algumas vítimas pelo mundo, é o RANSOMWARE — uma aplicação nociva, que não visa roubar dados do equipamento, mas “sequestrá-los”. O aplicativo age como um Vírus ou Trojan, mas uma vez infectado o equipamento, ele se mete no acesso aos dados do disco, e criptografa arquivos, pastas, o que ele acha que deve ou os dados para o qual ele foi programado para criptografar. Em um determinado momento, ele pode simplesmente restringir o acesso às pastas e arquivos, ou mesmo apagar os arquivos originais, e exigir um pagamento para o fornecimento da chave capaz de voltar os dados que ele criptografou na sua forma original, ou exige uma senha para o próprio programa malicioso desfazer e restaurar os dados dos arquivos originais — normalmente com o pagamento de alguma importância em dinheiro não rastreável, como cripto-moedas.

Estudo de caso – Agenda

Por hora a ideia de controle de acesso na Agenda, além de fins didáticos, é começar de forma simples, por exemplo, para tratar um cenário onde eu quero que apenas minha esposa e eu possamos abrir a Agenda, mas sem bloquear nenhuma operação ou contato da agenda. Nós dois podemos ver todas as informações de todos os contatos da Agenda, não importa quem cadastrou o contato, e podemos fazer todas as operações com os contatos — como alterar dados, alterar foto, excluir o contato, enfim.

Neste caso, precisamos inicialmente de uma nova tabela, com pelo menos dois campos, um para o nome ou identificação do usuário ou operador — pode ser um nome, ou um e-mail, e outro campo para a autenticação de acesso, como por exemplo uma senha. Antes de implementar isso, vamos avaliar nossa necessidade e as possibilidades de se implementar esta solução.

Usuário ou e-Mail ?

Por ser um sistema local, um nome único de usuário já resolveria a questão de identificação. Porém, pensando um pouco além do óbvio, eu bem que poderia usar um e-mail. Afinal, lembra do que acontece quando você “perde a chave”? Nem você entra mais na sua casa.

Vantagens do e-Mail

  • Se o próprio operador esquece da senha, e a aplicação é capaz de enviar um e-mail ao usuário, ele pode usar um recurso como por exemplo “Password Reset”, onde a aplicação gera uma chave temporária de acesso para aquele usuário, e envia a chave no e-mail dele. Somente ele deve ter acesso ao e-mail, logo, uma vez que ele acesse o sistema com a chave que o sistema gerou, a aplicação tenta garantir que é de fato aquele usuário que está realizando o acesso, e permite a ele redefinir sua senha de acesso.
  • Dificilmente um usuário se esquece do próprio endereço de e-mail.
  • Sem o e-mail, o usuário precisaria entrar em contato com o Administrador do Sistema, de alguma forma provar sua identidade, e pedir para ele uma troca de senha. Quando falamos de um sistema local de Agenda, pra usar em casa, sem problemas.  Mas, pensando em algo um pouco maior, algo como a Agenda disponível na Internet, é um desperdício criar um suporte para atender usuário que perdeu senha, se você pode dar a ele um procedimento seguro de provar sua identidade e restaurar sozinho e com segurança seu acesso ao sistema.

Autenticando por Senha

Antes de mais nada, a respeito de qualquer tipo de senha em um sistema informatizado: Nunca grave a senha em um sistema de controle de acesso. Sim, é isso mesmo: NÃO GRAVE A SENHA.

Certo, então como vêm a pergunta: Se eu não gravar a senha, como eu vou conseguir validar a senha que o operador digitou na entrada do sistema está certa ?! Bem, primeiro vamos aos riscos: Uma tabela de usuários e senhas vale muito para pessoas mal-intencionadas. Muitos usuários na internet usam senhas fortes para diversos recursos, mas acabam usando a mesma senha para outros recursos. Logo, mesmo que você grave a senha que o operador digitou usando algum tipo de criptografia, se alguém descobre o algoritmo de descriptografia, ele terá acesso a todas as senhas de todos os usuários.

Uma alternativa bastante segura é gerar um hash (ou dispersão criptográfica) unidirecional. Na prática, você usa um algoritmo que é capaz de gerar uma sequência de dados a partir de uma informação fornecida, mas o algoritmo não é capaz de restaurar o dado original a partir da sequência de dados (hash) gerado. Um bom exemplo disso é o MD5 — vide referências no final do post. A partir de uma informação fornecida, o algoritmo MD5 gera uma sequência hexadecimal de 128 BITS, mas a partir desta sequência ele não é capaz de desfazer a operação e informar qual foi a informação que gerou aquele hash.

Logo, quando o usuário informar a senha de acesso que ele gostaria de usar, a aplicação gera um MD5 desta senha, e grava o resultado no banco de dados, atrelado a este usuário. No momento que este usuário for entrar no sistema, e fornecer o e-Mail e Senha, você localiza ele no cadastro pelo e-Mail, e gera novamente o MD5 da senha que ele informou. Se ele informou a mesma senha, o resultado do MD5 será o mesmo. Assim, nem você sabe a senha original, nem quem conseguir roubar ou copiar esta tabela vai saber.

Existe a possibilidade de colisão, isto é, duas senhas diferentes gerarem o mesmo hash. Porém, como estamos falando de um hash de 128 bits, sabe quantas são as possíveis combinações? 2^128 (dois elevado a potência 128), algo em torno de 3.40e+38 combinações diferentes — imagine que 64 bits = 18.446.744.073.709.551.615 ( dezoito quintilhões, quatrocentos e quarenta e seis quatrilhões, setecentos e quarenta e quatro trilhões, setenta e três bilhões, setecentos e nove milhões, quinhentos e cinqüenta e um mil, seiscentas e quinze possibilidades), agora multiplica esse número por dois, sessenta e quatro vezes seguidas 😛 

Vamos ao AdvPL

Primeiro, vamos criar a função responsável pela criação e abertura da tabela de usuários. Ela é praticamente uma cópia com alterações da função de criação da tabela de Agenda.

// --------------------------------------------------------------
// Abertura da Tabela de USUARIOS da Agenda
// Cria uma tabela chamda "USUARIOS" no Banco de dados atual
// configurado no Environment em uso pelo DBAccess
// Cria a tabela caso nao exista, cria os índices caso nao existam
// Abre e mantém a tabela aberta em modo compartilhado
// --------------------------------------------------------------
STATIC Function OpenUsers()
Local cFile := "USUARIOS"
Local aStru := {}
Local aDbStru := {}
Local nRet

While !GlbNmLock("USUARIOS_DB")
  If !MsgYesNo("Existe outro processo abrindo a tabela USUARIOS. Deseja tentar novamente ?")
    MSgStop("Abertura da tabela USUARIOS em uso -- tente novamente mais tarde.")
    QUIT
  Endif
Enddo

// Cria o array com os campos do arquivo 
aadd(aStru,{"IDUSR" ,"C",06,0})
aadd(aStru,{"LOGIN" ,"C",50,0})
aadd(aStru,{"SENHA" ,"C",32,0})

If !TCCanOpen(cFile)
  // Se o arquivo nao existe no banco, cria
  DBCreate(cFile,aStru,"TOPCONN")
  Else
  // O Arquivo já existe, vamos comparar as estruturas
  USE (cFile) ALIAS (cFile) SHARED NEW VIA "TOPCONN"
  IF NetErr()
    MsgSTop("Falha ao abrir a tabela USUARIOS em modo compartilhado. Tente novamente mais tarde.")
    QUIT
  Endif
  aDbStru := DBStruct()
  USE

  If len(aDbStru) != len(aStru)
    // Estao faltando campos no banco ? 
    // Vamos alterar a estrutura da tabela
    // Informamos a estrutura atual, e a estrutura esperada
    If !TCAlter(cFile,aDbStru,aStru)
      MsgSTop(tcsqlerror(),"Falha ao alterar a estrutura da tabela USUARIOS")
      QUIT
    Endif
    MsgInfo("Estrutura do arquivo USUARIOS atualizada.")
  Endif

Endif

If !TCCanOpen(cFile,cFile+'_UNQ')
  // Se o Indice único da tabela nao existe, cria 
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  IF NetErr()
    MsgSTop("Falha ao abrir a tabela USUARIOS em modo EXCLUSIVO. Tente novamente mais tarde.")
    QUIT
  Endif
  nRet := TCUnique(cFile,"LOGIN")
  If nRet < 0 
    MsgSTop(tcsqlerror(),"Falha ao criar índice único")
    QUIT
  Endif
  USE
EndIf

If !TCCanOpen(cFile,cFile+'1')
  // Se o Indice por ID nao existe, cria
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  IF NetErr()
    MsgSTop("Falha ao abrir a tabela USUARIOS em modo EXCLUSIVO. Tente novamente mais tarde.")
    QUIT
  Endif
  INDEX ON IDUSR TO (cFile+'1')
  USE
EndIf

If !TCCanOpen(cFile,cFile+'2')
  // Se o indice por LOGIN nao existe, cria
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  IF NetErr()
    MsgSTop("Falha ao abrir a tabela USUARIOS em modo EXCLUSIVO. Tente novamente mais tarde.")
    QUIT
  Endif
  INDEX ON LOGIN TO (cFile+'2')
  USE
EndIf

// Abra o arquivo de agenda em modo compartilhado
USE (cFile) ALIAS (cFile) SHARED NEW VIA "TOPCONN"

If NetErr()
  MsgSTop("Falha ao abrir a tabela USUARIOS em modo compartilhado. Tente novamente mais tarde.")
  QUIT
Endif

// Abre os indices, seleciona ordem por ID
// E Posiciona no primeiro registro 
DbSetIndex(cFile+'1')
DbSetIndex(cFile+'2')
DbSetOrder(1)
DbGoTop()

// Solta o MUTEX 
GlbNmUnlock("USUARIOS_DB")

Return .T.

Para quem ainda está usando um binário 7.00.131227, que não têm as funções GlbNmLock() e GlbNmUnlock(), pode temporariamente substituí-las por GlbLock() e GlbUnlock() — a diferença é que o Lock realizado é global, isto é, desconsidera o nome informado como parâmetro.

Agora, vamos fazer a função de LOGIN, porém vamos atentar a um detalhe: Se eu não quiser habilitar o controle de usuários na minha agenda, eu simplesmente deixo o cadastro de usuários vazio.

// ---------------------------------------------------
// Função responsável pelo controle de acesso - Login
// Somente exige autenticação se o cadastro de usuários tiver 
// pelo menos um usuário 
// ---------------------------------------------------
STATIC Function ChkUser(oDlg)
Local lOk := .T.
// Abre cadastro de usuarios
OpenUsers()
// Vai para o topo do arquivo 
DbSelectarea("USUARIOS")
DBGoTOP()
If !EOF()
  // Se existem usuarios na tabela de usuarios, 
  // o login foi habilitado . 
  lOk := DoLogin(oDlg)
Endif
// Fecha o cadastro de usuarios 
DbSelectarea("USUARIOS")
USE 
If !lOk
  MsgStop("Usuário não autenticado.","Controle de Acesso")
  QUIT
Endif
Return

// Definições para uso da função AdvPL MD5()
#define RAW_DIGEST 1 
#define HEX_DIGEST 2

// Função responsável pelo diálogo e validação do Login
STATIC Function DoLogin(oDlg)
Local cTitle := 'Controle de Acesso'
Local oDlgLogin
Local oGetLogin
Local cLogin := space(50)
Local cPassW := space(16)
Local oBtnOk
Local lGo,lOk

While .T.
  lGo := .F.
  lOk := .F. 
  cLogin := space(50)
  cPassW := space(16)
  DEFINE DIALOG oDlgLogin TITLE (cTitle) ;
    FROM 0,0 TO 90,450 PIXEL;
    FONT oDlg:oFont ; // Usa a mesma fonte do diálogo anterior
    OF oDlg ;
    COLOR CLR_WHITE, CLR_RED

  @ 05+3,05 SAY oSay1 PROMPT "Login" RIGHT SIZE 20,12 OF oDlgLogin PIXEL
  @ 05,30 GET oGetLogin VAR cLogin PICTURE "@!" SIZE CALCSIZEGET(45) ,12 OF oDlgLogin PIXEL

  @ 25+3,05 SAY oSay1 PROMPT "Senha" RIGHT SIZE 20,12 OF oDlgLogin PIXEL
  @ 25,30 GET oGetPassw VAR cPassW SIZE CALCSIZEGET(16) ,12 OF oDlgLogin PIXEL
  oGetPassw:LPASSWORD := .T.

  @ 25,155 BUTTON oBtnOk PROMPT "Ok" SIZE 60,15 ;
    ACTION (lGo := .T. , oDlgLogin:End()) OF oDlgLogin PIXEL

  ACTIVATE DIALOG oDlgLogin CENTER

  If !lGo
    // SE a janela foi fechada, desiste 
    EXIT
  Endif

  DbSelectarea("USUARIOS")
  DBSetOrder(2) // Indice por LOGIN

  If DBSeek(cLogin)
    // Encontrou o Login informado
    If MD5(alltrim(cPassW),HEX_DIGEST) == USUARIOS->SENHA
      // A senha informada "bate" com a senha original
      // Seta que está OK, sai do Login
      lOk := .T.
      EXIT
    Endif 
  Endif

  // Chegou aqui, o login nao existe ou a senha nao confere 
  MsgStop("Login ou senha inválidos. "+;
    "Confirme os dados e repita a operação.", ;
    "Falha de Autenticação")

Enddo

Return lOk

Feito isso dessa forma, conseguimos implementar um controle de acesso simples e eficiente, e bastante seguro, pois a senha original nunca é armazenada. A tela, após implementado um usuário, deve ficar assim:

CRUD - Controle de Acesso

Conclusão

Por hora, sem a inclusão de um usuário, não há autenticação na Agenda. As partes de código publicadas aqui ainda exigem alguns ajustes em outros pontos, por exemplo inserir o Login na inicialização da Agenda. Para pegar o fonte completo, acesse o GITHUB!

Agradeço novamente a audiência, as curtidas e os comentários, e desejo novamente a todos TERABYTES DE SUCESSO 😀 

Referências

 

Dicas valiosas de programação – Parte 04

Introdução

Continuando o assunto de dicas valiosas de programação, vamos abordar alguns assuntos relacionados a JOBS (Programas sem interface), pontos de atenção, alternativas de controle, etc.

Considerações sobre JOBS

Em tópicos anteriores, vimos que existem várias formas de subir um ou mais jobs em um serviço do Protheus Server. A maior dificuldade dos JOBS consiste em saber o que ele está fazendo, e como ele está fazendo. O fato do Job não ter nenhum tipo de interface torna esse trabalho um pouco diferente dos demais programas.

Quando usamos um pool de JOBS, como por exemplo os processos de WebServices ou de portais WEB (JOB TYPE=WEBEX), definimos o número inicial (mínimo) de processos, numero máximo, e opcionalmente mínimo livre e incremento. Logo, não precisamos nos preocupar se o servidor recebe mais uma requisição e todos os processos que estão no ar estão ocupados — se o número máximo de processos configurado não foi atingido, o próprio Protheus Server coloca um ou mais jobs no ar.

Gerando LOG de um JOB

Normalmente quando precisamos acompanhar o que um determinado JOB está fazendo, podemos usar o IDE e/ou TDS para depurar o Job, ou caso isto não seja uma alternativa para a questão, o programa pode ser alterado para emitir mensagens a cada etapa de processamento. Uma das alternativas normalmente usadas — e mais simples de usar — é usar a função AdvPL conout() nos programas envolvidos, para registar no log de console do Protheus Server mensagens sobre o que cada processo está fazendo. Para diferenciar os processos, podemos usar a função ThreadID() do AdvPL, para identificar o número da thread que gerou a mensagem.

Outra alternativa interessante, inclusive mais interessante que usar o log de console do servidor de aplicação, é fazer com que o job crie um arquivo de LOG dele próprio em disco, usando por exemplo a função fCreate(), criando o arquivo em uma pasta a partir do RootPath do ambiente, usando por exemplo um prefixo mais o numero da thread atual mais o horário de inicio do job como nome do arquivo — para ficar fácil saber quais logs são de quais JOBS — e gravar os dados de LOG dentro desse arquivo usando a função fWrite()  — lembrando de inclusive gravar os caracteres chr(13)+chr(10) ao final de cada linha — estes caracteres de controle indicam uma quebra de linha em um arquivo no padrão Windows. Para Linux, a quebra de linha padrão é apenas chr(10).

Acompanhando a execução de um JOB

Quando você cria um determinado JOB para alguma tarefa longa, pode ser interessante saber em que ponto ou etapa da tarefa o JOB está trabalhando em um determinado momento. A solução mais leve, é você criar um nome de uma variável global — aquelas que são acessadas através das funções PutGlbVars() e GetGlbVars() — e alimentar dentro do JOB a variável global com a etapa atual do processo, enquanto um outro programa (em outro processo, com interface por exemplo) consulta a variável para saber qual é a tarefa interna do Job em andamento.

Desta forma, um programa externo pode consultar — através de variáveis globais com nomes pré-definidos — o status de não apenas um, mas vários jobs sendo executados no servidor de aplicação atual. Basta criar identificadores únicos não repetidos antes de iniciar os processos.

Ocorrências de Erro Críticas

Mesmo que o seu JOB possua um tratamento de erro, cercado com BEGIN SEQUENCE … END SEQUENCE e afins, as ocorrências de erro de criticidade mais alta não são interceptadas ou tratadas. Desse modo, se você apenas consulta uma variável global para pegar o status de um Job, ele pode ter sido derrubado ou ter finalizado com uma ocorrência critica de erro, e o programa que está esperando ou dependendo de um retorno dele nem sabe que ele já não está mais sendo executado.

Não há contorno para tentar manter no ar um JOB que foi finalizado por uma ocorrência crítica, porém você pode descobrir se ele ainda está no ar ou não, usando alguns recursos, por exemplo:

  1. Além da variável global para troca de status, faça o JOB obter por exemplo um Lock Virtual no DBAccess ou um Lock em Disco — busque no blog por “MUTEX” e veja algumas alternativas. A ideia é usar um recurso nomeado em modo exclusivo, que é liberado automaticamente caso o JOB seja finalizado por qualquer razão. Se o seu programa que espera retorno do JOB está sem receber nenhuma atualização, verifique se o JOB está no ar tentando fazer um bloqueio do recurso que o JOB está usando. Se o seu processo conseguiu o bloqueio, o JOB foi pro vinagre…
  2. Verifique se o seu processo ainda está no ar usando — com moderação — por exemplo a função GetUserInfoArray() — ela retorna um array com as informações dos processos em execução na instância atual do Protheus Server. Para isso, pode ser necessário que o JOB que foi colocado em execução use uma variável global para o processo principal e de controle de jobs saber qual é o ThreadID deste processo, para ser possível um match com o retorno da GetUserInfoArray().

Seu processo principal pode não saber o que aconteceu com o processo filho, mas sabe que ele não está mais no ar, e saiu antes de dar um resultado. Isso muitas vezes é suficiente para você estudar uma forma de submeter o processo novamente, ou de encerrar o processo principal informando que houve um término anormal e os logs de erro do sistema devem ser verificados, ao invés de esperar para sempre um JOB que já não está mais lá.

Conclusão

Quanto mais nos aprofundamos em um tema, mais temas aparecem para nos aprofundarmos 🙂 E, é claro que veremos exemplos de uso práticos destes mecanismos, com fonte e tudo, nos próximos posts !!!

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

Referências

 

Dicas valiosas de programação – Parte 03

Introdução

Neste post, vamos a uma dica muito importante e específica do AdvPL: Como proteger a chamada de novas funções implementadas na linguagem AdvPL — e evitar o erro “function <xxx> has compilation problems. Rebuild RPO”

Funções do AdvPL

Ao escrevermos uma aplicação em AdvPL, os desenvolvedores podem usar funções básicas da linguagem AdvPL, que por serem nativas da linguagem, estão compiladas e publicadas dentro do Application Server (executável/dlls), e as funções de Framework e Produto, compiladas dentro do RPO (Repositório de funções e classes AdvPL).

Compilation Problems …

Quando é criada uma nova função básica da linguagem AdvPL, ela estará disponível para uso apenas quando você atualizar o seu Application Server para uma build igual ou superior a qual a função foi disponibilizada. O problema é que, uma vez que você implemente uma chamada desta nova função dentro de um código AdvPL, para que esta chamada funcione, você precisa compilar o seu fonte usando o binário mais novo — que já têm a função — e executar o seu fonte com ele.

Caso você compile seu fonte com um binário mais antigo — que não tem a função — e tente executar o fonte em um binário mais novo, ou faça o contrário — compile usando um binário novo e tente executar em um binário antigo, ocorre o erro “function <xxx> has compilation problems. Rebuild RPO”. E este erro não ocorre durante a execução do fonte, mas ocorre durante a carga da função na memória para ser executada.

Este comportamento é uma amarração de segurança, para evitar que inadvertidamente uma função AdvPL compilada no RPO possa conflitar — ou mesmo tentar sobrescrever — uma função básica da linguagem AdvPL.

Onde isso pode ser um problema ?

Imagine que você dá manutenção em um código já existente e que funciona, e em uma build mais nova do Application Server, uma função nova foi publicada, que pode tornar o seu processo mais rápido.  A mão “coça” pra implementar a chamada da função, porém você não sabe se esse fonte será compilado com um binário mais velho ou novo, nem como garantir que o seu cliente vá atualizar a Build do Application Server para rodar o código novo. Neste caso, você precisa que seu código continue funcionando em uma build antiga ou nova, e independentemente se ele foi compilado em uma build nova ou não.

Como fazer a implementação protegida ?

São apenas alguns passos simples, mas que precisam ser feitos desta forma.

  • Verifique se a nova função existe em tempo de execução, chamando a função FindFunction(), passando como parâmetro entre aspas o nome da função.  Caso a função exista na build em uso, ela retorna .T.
  • Coloque o nome da função nova da linguagem AdvPL que você quer chamar, dentro de uma variável local caractere — por exemplo cFuncName
  • Após verificar se a função existe, faça a chamada para esta função usando o operador de macro-execução, com a seguinte sintaxe:

[<retorno> := ] &cFuncName.([parm1][,parm2][,…])

Por exemplo, vamos imaginar que você quer usar uma nova função do AdvPL, chamada FindClass() — que serve para dizer se uma determinada classe existe no ambiente onde a função está sendo executada.

user function tstfindcls()
Local cFnName := 'findclass'
Local lExist := .F.
If FindFunction(cFnName)
   lExist := &cFnName.("TGET")
   If lExist
      MsgInfo("A Classe TGET existe")
   Else
      MsgStop("A classe TGET nao existe")
   Endif
Else
   MsgStop("Função "+cFnName+" não encontrada", ;
           "Atualize o Application Server")
Endif
Return

Dessa forma, mesmo que você compile esse fonte AdvPL em uma build antiga ou nova, ele vai rodar corretamente em uma build nova, e caso seja executado em uma build antiga — que ainda não tenha a função FindClass — o fonte não vai apresentar erro de carga, mas vai identificar que a função não existe e mostrar a mensagem desejada.

Conclusão

Espero que esta dica, mesmo que  “curtinha”, seja de grande valia para todos os desenvolvedores AdvPL. Desejo novamente a todos TERABYTES de sucesso !!! 😀

Referências