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

 

Anúncios

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

 

 

CRUD em AdvPL – Parte 15

Introdução

No post anterior, foi feito um Controle de Acesso para a Agenda, certo? Porém, o controle ainda não funciona, pois não foi feita a rotina de manutenção do cadastro de usuários. Logo, vamos criar esta rotina, mas antes disso vamos ver um pouco sobre reaproveitamento de código. Assim, quando partirmos para a rotina de manutenção de usuários não vamos precisar de tantas linhas de código como foi necessário na Agenda.

Reaproveitamento de Código

Quando você programa de modo segmentado, deixando claro o papel de cada função, mesmo que você não use OO (Orientação a Objetos), é relativamente fácil reaproveitar uma função para uma mesma tarefa, feita por entidades do programa diferentes.

Vamos começar pelo acesso aos dados. No programa AGENDA.PRW, existem duas funções, chamadas de OpenDB() e CloseDB(). Ambas, como foram inicialmente projetadas para serem chamadas apenas de dentro do fonte AGENDA.PRW, foram declaradas como STATIC FUNCTION. De fato, o uso de uma STATIC FUNCTION inicialmente limita a chamada desta função para outras funções ou mesmo métodos de classes, desde que eles sejam escritos dentro do mesmo arquivo fonte.

Porém, quando utilizamos o Protheus como plataforma de desenvolvimento, sem uma autorização especial fornecida pela TOTVS, você não consegue declarar uma FUNCTION no código, que poderia ser chamada de outros arquivos fonte. Quando não temos nenhuma autorização diferenciada de compilação, o ambiente Protheus permite a compilação de CLASSES ADVPL,  STATIC FUNCTION(s) e USER FUNCTION(s).

Escopo e Chamada de Funções

Quando declaramos uma USER FUNCTION, por exemplo USER FUNCTION AGENDA(), na verdade a função compilada no repositório de objetos chama-se U_AGENDA — É inserido o prefixo “U_” no nome da função. Uma USER FUNCTION pode ser chamada entre fontes distintos, porém reduz ainda mais a parte útil do nome da função. Exceto quando programamos em TL++ (ainda vamos falar disso em outro post), o AdvPL padrão permite a declaração de funções com até 10 caracteres em seu nome. Logo, quando usamos uma USER FUNCTION, o prefixo “U_” já usa dois caracteres, sobrando apenas 8 caracteres para identificar a função.

Você pode inclusive declarar a função com mais de 10 caracteres, e chamar ela com mais de 10 caracteres, PORÉM somente os 10 primeiros caracteres serão considerados, tanto na declaração quanto na chamada. Por exemplo, declare uma função chamada USER FUNCTION MEUTESTEOK(), e agora, de dentro de outra função, chame U_MEUTESTEX() — a função U_MEUTESTEOK será chamada. A função real compilada no Repositório vai se chamar U_MEUTESTE — com apenas os 10 caracteres significativos — então, se você chamar a função como U_MEUTESTE1 ou U_MEUTESTEX, são considerados apenas os 10 caracteres iniciais do nome da função, inclusive os dois caracteres do prefixo “U_”.

Quando utilizado TL++, onde existe suporte para funções, variáveis, classes, proriedades e métodos com mais de 10 caracteres — acho que vai até 250 — um fonte escrito assim daria erro, pois a chamada da função deve conter o nome inteiro igual a declaração da mesma.

Escopo de Classes AdvPL

Como as classes no AdvPL têm um fator de dinamismo bem alto, e não é necessário possuir um arquivo auxiliar ou #include com a definição da classe para ser possível consumir a classe em AdvPL, basta saber o nome da classe, seus métodos e propriedades para chamar seus construtores, métodos e propriedades, de qualquer arquivo fonte compilado no repositório.

De volta ao reaproveitamento

Se eu quiser usar as funções OpenDB() e CloseDB(), hoje declaradas como STATIC FUNCTION, em mais de um código ou programa fonte do projeto, eu preciso redefinir esta função.

A primeira alternativa poderia ser alterar a declaração de STATIC FUNCTION OPENDB() para USER FUNCTION OPENDB(), e trocar no fonte atual todas as chamadas dela, que originalmente usavam apenas OpenDb(), para o novo nome da função, que passou a se chamar U_OPENDB()

Outra alternativa — mais elegante — seria criar uma classe para agrupar estas funcionalidades — conexão com um Banco de Dados relacional através do DBAccess — e declarar estas funções como métodos da classe, para então criar uma instância da classe no início da execução do código, e fazer a chamada dos seus métodos no lugar das funções.

Embora ambas atendam a nossa necessidade, vamos optar pela mais elegante, vamos criar uma classe, em um novo fonte AdvPL, que deverá ser compilado no Projeto, para então utilizá-lo onde for necessário, em qualquer fonte do projeto.

Funções OpenDB() e CloseDB()

Vamos ver como eram estas funções:

STATIC Function OpenDB()
// Conecta com o DBAccess configurado no ambiente
nH := TCLink()
If nH < 0
  MsgStop("DBAccess - Erro de conexao "+cValToChar(nH))
  QUIT
Endif
// Liga o filtro para ignorar registros deletados na navegação ISAM 
SET DELETED ON
Return

STATIC Function CloseDB()
DBCloseAll()   // Fecha todas as tabelas
Tcunlink()     // Desconecta do DBAccess
Return

Agora, vamos ver como elas ficariam sendo escritas como métodos de uma classe — que vamos chamar de DBDriver,  criada no novo fonte DBDRIVER.PRW:

CLASS DBDriver
   DATA nHnd as INTEGER 
   METHOD New() CONSTRUCTOR 
   METHOD OpenDB()
   METHOD CloseDB()
ENDCLASS

METHOD New() CLASS DBDriver
::nHnd := -1
return self

METHOD OpenDB() CLASS DBDriver
If ::nHnd > 0 
  // Já estou conectado, torna esta conexão ativa 
  TCSetConn(::nHnd) 
  Return .T. 
Endif
// Conecta com o DBAccess configurado no ambiente
::nHnd := TCLink()
If ::nHnd < 0
  MsgStop("DBAccess - Erro de conexao "+cValToChar(::nHnd))
  Return .F.
Endif
// Liga o filtro para ignorar registros deletados na navegação ISAM 
SET DELETED ON
Return .T.

METHOD CloseDB() CLASS DBDriver
If ::nHnd > 0 
   DBCloseAll() // Fecha todas as tabelas e alias 
   Tcunlink(::nHnd) // Encerra a conexao atual com o DBAccess
   ::nHnd := -1 
Endif
Return .T.

Reparem no que houve, Eu criei um novo código fonte, chamado DBDRIVER.PRW, e dentro dele transformei as funções OpenDB() e CloseDB() em dos métodos. Depois, nos pontos do código onde as antigas funções eram chamadas, eu passsei a criar uma instância do Driver em uma variável local, chamada oDBSrv, chamando o construtor do Driver — DBDriver():New() — e nas chamadas das funções OpenDB() e CloseDB() , eu passei a chamar elas como métodos da classe, usando oDBSrv:OpenDB() e oDBSrv:CloseDB(). Dessa forma, eu estou isolando estas funcionalidades em um fonte separado, para ser chamado por outros fontes que precisem realizar as mesmas tarefas, como o fonte da Agenda e o fonte do Cadastro de Usuários.

Reaproveitando mais …

Ao implementar o cadastro de usuários na Agenda, a função OpenUsers() foi praticamente uma cópia da OpenAgenda(), trocando o nome da tabela e dos campos. Que tal fazermos dela apenas um método na nova classe DBDRiver?

METHOD OpenTable(cFile,aStru,cIdxUnq,aIndex) CLASS DBDriver
Local cLockId , aDbStru
Local nI , nIndex, cIndex

// Ajusta nome da tabela e cria 
// identificador de Bloqueio de operação
cFile := Upper(cFile)
cLockId := cFile+"_DB"

If empty(aIndex)
   aIndex := {}
Endif
// Quantos indices permanentes tem esta tabela 
nIndex := len(aIndex)

While !GlbNmLock(cLockId)
  If !MsgYesNo("Existe outro processo abrindo a tabela "+cFile+". "+;
               "Deseja tentar novamente ?")
    MsgStop("Abertura da tabela "+cLockId+" em uso -- "+;
           "tente novamente mais tarde.")
    Return .F. 
  Endif
Enddo

If !TCCanOpen(cFile)
  // Se o arquivo nao existe no banco, cria
  DBCreate(cFile,aStru,"TOPCONN")
Endif

// O Arquivo já existe, vamos comparar as estruturas
USE (cFile) ALIAS (cFile) SHARED NEW VIA "TOPCONN"
IF NetErr()
  MsgSTop("Falha ao abrir a tabela "+cFile+" em modo compartilhado. "+;
          "Tente novamente mais tarde.")
  Return .F.
Endif
// Obtêm a estrutura do banco de dados 
aDbStru := DBStruct()
USE

If len(aDbStru) != len(aStru)
  // O tamanho das estruturas mudou ?
  // 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 "+cFile)
    Return .F. 
  Endif
  MsgInfo("Estrutura do arquivo "+cFile+" atualizada.")
Endif

// Se esta tabela deve ter indice unico
// Cria caso nao exista 
If !empty(cIdxUnq)
  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 "+cFile+" em modo EXCLUSIVO. "+;
              "Tente novamente mais tarde.")
      Return .F.
    Endif
    nRet := TCUnique(cFile,cIdxUnq)
    If nRet < 0
      MsgSTop(tcsqlerror(),;
              "Falha ao criar índice único ["+cIdxUnq+"] "+;
              "para a tabela ["+cFile+"]")
      Return .F.
    Endif
    USE
  EndIf
Endif

// Cria os indices que nao existem para a tabela \
// a Partir do Array de indices informado
For nI := 1 to nIndex
  // Determina o Nome do Indice
  cIndex := cFile+cValToChar(nIndex)
  // Pega a expressão de indexação
  cIdxExpr := aIndex[nI]
  If !TCCanOpen(cFile,cIndex)
    // Se o Indice nao existe, cria 
    USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
    IF NetErr()
      MsgSTop("Falha ao abrir a tabela "+cFile+" em modo EXCLUSIVO. "+;
              "Tente novamente mais tarde.")
      Return .F.
    Endif
    INDEX ON &cIdxExpr TO (cIndex)
    USE
  Endif
EndIf

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

If NetErr()
  MsgSTop("Falha ao abrir a tabela "+cFile+" em modo compartilhado. "+;
          "Tente novamente mais tarde.")
  Return .F.
Endif

// Abre os indices, seleciona o primeiro
// e posiciona o arquivo no topo

For nI := 1 to nIndex
  cIndex := cFile+cValToChar(nIndex)
  DbSetIndex(cIndex)
Next

DbSetOrder(1)
DbGoTop()

// Solta o MUTEX
GlbNmUnlock(cLockId)

Return .T.

Reparem que tudo o que estava pré-definido em código (ou “CHUMBADO”) agora é recebido por parâmetro. cFile recebe o nome do arquivo, aStru recebe o array com a estrutura da tabela no formato AdvPL, cIdxUnq quando informado deve conter os campos a serem usados para a criação de índice único no SGDB separados por vírgula, e aIndex deve ser um Array de Strings, onde cada elemento corresponde a expressão de indexação para cada índice que deve ser criado para esta tabela.

Vamos ver agora como ficaria a nova função OpenAgenda(), usando o método OpenTable() da classe DBDriver?

STATIC Function OpenAgenda(oDbDrv)
Local cFile := "AGENDA"
Local aStru := {}
Local cIdxUnq 
Local aIndex := {}
Local lOk

// Define a estrutura do arquivo 
aadd(aStru,{"ID"    ,"C",06,0})
aadd(aStru,{"NOME"  ,"C",50,0})
aadd(aStru,{"ENDER" ,"C",50,0})
aadd(aStru,{"COMPL" ,"C",20,0})
aadd(aStru,{"BAIRR" ,"C",30,0})
aadd(aStru,{"CIDADE","C",40,0})
aadd(aStru,{"UF"    ,"C",02,0})
aadd(aStru,{"CEP"   ,"C",08,0})
aadd(aStru,{"FONE1" ,"C",20,0})
aadd(aStru,{"FONE2" ,"C",20,0})
aadd(aStru,{"EMAIL" ,"C",40,0})
aadd(aStru,{"IMAGE" ,"M",10,0})

// Índice único pelo campo ID 
cIdxUnq := "ID"

// Define a estrutura de índices
aadd(aIndex,"ID") // Indice 01 por ID 
aadd(aIndex,"NOME") // Indice 02 por NOME

// Pede para o Driver de Dados abrir a tabela 
lOk := oDbDrv:OpenTable(cFile,aStru,cIdxUnq,aIndex)

Return lOk

Reparem que eu passei tudo como parâmetro para o Driver. Sim, eu também passei a receber como parâmetro o Driver de Dados da aplicação como parâmetro. Mas meu código, com mais de 100 linhas, caiu para 32 linhas ou menos. Ao fazer a mesma coisa com a função OpenUsers(), economizamos mais um montão de linhas e reaproveitamos o código de criação e abertura de tabelas e índices.

STATIC Function OpenUsers(oDbDrv)
Local cFile := "USUARIOS"
Local aStru := {}
Local cIdxUnq 
Local aIndex := {}
Local lOk

// 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})

// Índice único pelo campo LOGIN
cIdxUnq := "LOGIN"

// Definição de índices
aadd(aIndex,"IDUSR") // Indice 01 por ID de Usuario 
aadd(aIndex,"LOGIN") // Indice 02 por Login

// Pede para o Driver de Dados abrir a tabela 
lOk := oDbDrv:OpenTable(cFile,aStru,cIdxUnq,aIndex)

Return lOk

Não ficou bem mais simples? O código apenas preencheu as definições necessárias, e a responsabilidade de fazer a mágica é do Driver. De qualquer modo, a simplicidade dos argumentos informados parte de algumas premissas e comportamentos da versão atual deste fonte:

  1. O Método OpenTable() vai criar os índices usando o nome da tabela informado como parâmetro, seguido por um número, iniciando em “1”, para os índices informados, na ordem em que foram informados.
  2. O índice único segue a nomenclatura padrão do DBACCESS — Nome da tabela mais o sufixo “_UNQ”.
  3. A análise realizada na abertura da tabela comparando a diferença entre a estrutura da tabela definida pelo código e a estrutura atual do banco de dados — na versão atual — apenas verifica se o tamanho delas está diferente, não levando em conta a possibilidade de haver alteração de propriedades de campos existentes.
  4. A verificação dos índices limita-se a criar índices inexistentes. Caso seja necessária uma alteração de alguma expressão de indexação, ainda não há verificação para isso na aplicação, devendo o índice ser removido do SGDB para ser recriado pela aplicação.

E, por mais bonito que tenha ficado o fonte desta classe, ele ainda têm uma característica indesejável: Ele ainda faz comunicação direta com a interface, através de funções como MsgInfo(), MsgStop() e afins. Mas não criemos pânico, logo logo vamos refatorar este código também…

Conclusão

Acho que o reaproveitamento do código atual vai precisar de mais uma etapa, antes de partirmos para a inclusão de um novo usuário. Porém, podemos fazer um AJUSTE PONTUAL na rotina, criando um usuário “Inicial”, caso você deseje usar e testar o mecanismo de LOGIN. Para isso, altere a função ChkUser() para ela fazer a inserção do ADMIN com senha em branco caso a tabela esteja fazia, e a partir de então sempre solicitar o login, veja a parte do fonte alterado abaixo:

// Vai para o topo do arquivo 
DbSelectarea("USUARIOS")
DBGoTOP()

If EOF()
   // Se nao tem usuarios , cria um "ADMIN", com senha em branco 
   DBAppend()   
   USUARIOS->IDUSR := '000000'
   USUARIOS->LOGIN := "ADMIN"
   USUARIOS->SENHA := MD5("",HEX_DIGEST)
   DBRUnlock()
Endif

// Faz a validação do usuário 
lOk := DoLogin(oDlg)

No próximo post, vamos continuar refatorando alguns pontos da Agenda para reaproveitar mais código, assim ao invés de COPIAR E COLAR — e replicar código DESNECESSARIAMENTE — usamos a refatoração para reaproveitar uma parte das funcionalidades já escritas, que serão comuns para novos componentes e tabelas da aplicação.

Desejo a todos um bom aproveitamento desse material, e TERABYTES DE SUCESSO 😀

 

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

 

CRUD em AdvPL – Parte 13

Introdução

Nos tópicos anteriores, funcionalidades e recursos foram adicionados ao programa de Agenda. Agora, vamos criar uma “casca” de acesso sobre este fonte, e futuras funcionalidades? Que tal uma aplicação com uma MAIN WINDOW?

Criando o AdvPLSuite

Vamos criar um fonte AdvPL, que vai criar uma WINDOW, ao invés de uma DIALOG. Como somente pode haver uma janela do tipo WINDOW na aplicação AdvPL, ela será a base para ter um Menu de Opções, para chamar os demais programas que serão publicados aqui no Blog, seguindo a linha do CRUD, com o programa Agenda. Segue abaixo o fonte ADVPLSUITE.PRW

#include "protheus.ch"

#define CALCSIZESAY( X ) (( X * 4 ) + 4)

// --------------------------------------------------------------
// Programa principal AdvPLSuite
//
// Autor Júlio Wittwer
// Data 28/10/2018
//
// Serve de Menu para os demais programas de exemplo do Blog 
// Primeiro programa acrescentado : U_AGENDA
// --------------------------------------------------------------

User Function AdvPLSuite()
Local oMainWnd
Local cTitle := "AdvPL Suite 1.00"
Local oFont

// Define Formato de data DD/MM/AAAA
SET DATE BRITISH
SET CENTURY ON

// Usa uma fonte Fixed Size
oFont := TFont():New('Courier new',,-14,.T.)

// Seta que esta é a fonte DEFAULT para Interface
SETDEFFONT(oFont)

DEFINE WINDOW oMainWnd FROM 0,0 to 768,1024 PIXEL ; 
TITLE (cTitle) COLOR CLR_WHITE, CLR_BLUE

// Monta o menu superior da aplicaçáo 
BuildMenu(oMainWnd)

// Executa rotina de entrada 
oMainWnd:bStart := {|| WndStart(oMainWnd) }

// Ativa a janela principal maximizada 
ACTIVATE WINDOW oMainWnd MAXIMIZED

Return


// --------------------------------------------------------------
// Montagem do Menu do AdvPLSuite 
// --------------------------------------------------------------
STATIC Function BuildMenu(oMainWnd)
Local oMenuBar
Local oTMenu1, oTMenu2

conout('BuildMenu - IN')

// Cria a barra superior de Menus da Aplicação na Janela Principal
oMenuBar := tMenuBar():New(oMainWnd)
oMenuBar:SetColor(CLR_BLACK,CLR_WHITE)

// Cria o menu de Programas e acrescenta os itens
oTMenu1 := TMenu():New(0,0,0,0,.T.,,oMainWnd,CLR_BLACK,CLR_WHITE)
oMenuBar:AddItem('Programas' , oTMenu1, .T.)

oTMenu1:Add(TMenuItem():New(oMainWnd,'&Agenda',,,,{||U_AGENDA()},,,,,,,,,.T.))
oTMenu1:Add(TMenuItem():New(oMainWnd,'Sai&r',,,,{||oMainWnd:End()},,,,,,,,,.T.))

// Cria o Menu de Ajuda e acrescenta Itens
oTMenu2 := TMenu():New(0,0,0,0,.T.,,oMainWnd,CLR_WHITE,CLR_BLACK)
oMenuBar:AddItem('Ajuda', oTMenu2, .T.)

oTMenu2:Add(TMenuItem():New(oMainWnd,'&TDN - AdvPL',,,,{||OpenURL("http://tdn.totvs.com/display/tec/AdvPL")},,,,,,,,,.T.))
oTMenu2:Add(TMenuItem():New(oMainWnd,'&Sobre',,,,{||HlpAbout()},,,,,,,,,.T.))

Return

// --------------------------------------------------------------
// Tela de inicialização -- Mensagem de Entrada
// --------------------------------------------------------------
STATIC Function WndStart(oMainWnd)
Local oDlg
Local cTitle
Local cMsg

cTitle := "Bem vindo ao AdvPL Suite 1.00"

DEFINE DIALOG oDlg TITLE (cTitle) ;
FROM 0,0 TO 100,400 ;
COLOR CLR_WHITE, CLR_RED PIXEL

@ 05,05 SAY "APPServer Build .... "+GetBuild() SIZE CALCSIZESAY(50),12 OF oDlg PIXEL 
@ 18,05 SAY "Smartclient Build .. "+GetBuild(.T.) SIZE CALCSIZESAY(50),12 OF oDlg PIXEL

cMsg := "SERVER "

If IsSrvUnix()
  cMsg += 'LINUX '
Else
  cMsg += 'WINDOWS '
Endif

if ISSRV64()
  cMsg += '64 BITS'
Else
  cMsg += '32 BITS'
Endif

@ 31,05 SAY cMsg SIZE CALCSIZESAY(50),12 OF oDlg PIXEL

ACTIVATE DIALOG oDlg CENTER

Return

// --------------------------------------------------------------
// Mensagem informativa sobre o autor
// --------------------------------------------------------------

STATIC Function HlpAbout()
MsgInfo("<html><center>AdvPL Suite V1.00<hr>Júlio Wittwer<br><b>Tudo em AdvPL</b>")
return

// --------------------------------------------------------------
// Encapsulamento de abertura de URL
// Abre a URL informada como parametro no navegador padrão
// da máquina onde está sendo executado o SmartClient
// --------------------------------------------------------------
STATIC Function OpenURL(cUrl)
shellExecute("Open", cUrl, "", "", 1 )
return

Após compilar este código, e executá-lo diretamente pelo SmartClient, chamando a função U_ADVPLSUITE, o resultado esperado é ser aberta uma janela na tela, com o título “AdvPL Suite 1.00“, com fundo azul, e dentro dela uma caixa de diálogo com fundo vermelho, mostrando algumas informações.

Suite - Splash

Após pressionar a tecla [ESC] ou clicar no “X” para encerrar o diálogo, vamos acessar o menu no canto superior esquerdo, e ir na opção Programas –> Agenda. A aplicação de agenda deve ser aberta na tela, centralizada.

Suite - Agenda

Reparem que a Agenda está com o alinhamento diferente dos botões de navegação, resolvi fazer uma alteração no LayOut dos painéis, foi fácil fazer isso e eu não precisei alterar nenhuma coordenada dos componentes de interface.

Os códigos atualizados do post atual e da agenda estão disponíveis para Download no GITHUB — https://github.com/siga0984/Blog/, pode entrar e baixar, compilar, alterar, etc!!!

Conclusão

Prevendo o crescimento da aplicação, agora já temos um “Menu” para acrescentar funcionalidades. Agenda será apenas uma delas, mas ao longo do tempo, novas serão inseridas e integradas. Aguardem as cenas do próximo capítulo.

Desejo novamente a todos TERABYTES DE SUCESSO 😀

Referências