segunda-feira, 22 de junho de 2015

Capturando eventos de Logon e Logoff através de uma aplicação WindowsService

Olá Pessoal,

Estou tirando a poeira do blog trazendo um assunto que me queimou uns bons neurônios nestes últimos dias.

Estou desenvolvendo uma aplicação em C# com .NET4 do tipo WindowsService que irá realizar diversas funções em um computador rodando Windows. Uma destas funções é registrar os logons e logoffs dos usuários que utilizarem tais computadores em um banco de dados SQL Server remoto para posterior consulta e auditoria.

Pesquisando na web, encontrei diversas formas de codificar a captura destes eventos. Enquanto algumas se mostraram muito simples, outras eram bem complexas e cheias de código. Por fim, elas não apresentaram o resultado como eu gostaria de visualizar.

Depois de alguns dias escovando bit, cheguei a uma solução utilizando um pouco de tudo que li nesses dias. O resultado eu gostaria de compartilhar com vocês e, caso conheçam uma forma mais simples, não deixem de postar.Afinal de contas, o blog foi criado para compartilharmos informações.

Vamos então à minha solução:

Toda classe derivada de ServiceBase possui a propriedade CanHandleSessionChangeEvent que quando definida como true, faz com que a classe derivada seja notificada caso haja alterações nas sessões do Windows.

Para tratar as notificações, devemos implementar o seguinte evento:






A estrutura SessionChangeDescription possui 2 propriedades:

  • Reason: descreve o tipo do evento (SessionLogon, SessionLogoff, RemoteConnect, RemoteDisconnect, SessionLock, SessionUnlock, dentre outros);
  • SessionId: contém o número da sessão em que o evento ocorreu (1, 2, 3, ...).
Até aqui foi sopinha no mel...

A coisa começou a complicar quando tentei descobrir qual era o usuário conectado na sessão do evento e descobri que no .NET não há uma forma "nativa" de fazer isso. Nem uma classe, método, evento, nada... Pelo menos, eu não encontrei. E olha que revirei vários links do Google.

O máximo que encontrei, foi o pessoal utilizando APIs de Terminal Services com códigos extremamente grandes fazendo chamadas a DLLs externas e que ainda sim, não traziam o resultado que eu queria: Listavam os usuários conectados, mas não as sessões. 

Ou eu não fiz a codificação direita ou deixei algo passar despercebido ou eu sou muito burro mesmo, mas enfim... Não fui muito feliz com o lance das APIs. Além do que, parece que essas APIs são compatíveis apenas com o Vista ou superior e eu preciso que o código rode também no XP. É o XP ainda não morreu por aqui...

Foi então que voltei minha atenção a uma tecnologia que tenho utilizado nos últimos anos e que tem me ajudado bastante: Windows Management Instrumentation ou simplesmente WMI.

Pesquisando as classes do namespace root\CIMv2 encontrei a classe Win32_Process que lista os processos atualmente em execução no computador e, ao selecionar um processo específico, eu tenho a propriedade SessionId que mostra em qual sessão o processo está sendo executado. E para minha alegria esta mesmíssima classe possui um método chamado GetOwner que retorna o nome do usuário que está executando aquele processo... Bingo! Bastava agora encontrar uma forma de filtrar os processos. E minha solução para todo este caos foi a seguinte:

Antes de mais nada, adicione System.Management à lista de referências do seu projeto.

Criei uma estrutura simples para armazenar as sessões de usuário e declarei-a no escopo do meu serviço:



Criei um método para atualizar as sessões:

















E um método para esperar por uma sessão:











No OnStart do serviço, chamei o método RefreshUserSessions() para que o serviço saiba quais sessões já estão ativas no computador. E reescrevi o evento OnSessionChange da seguinte forma:



















Na parte de //Código... entra a minha codificação para o banco de dados. Mas para saber qual é o usuário da sessão basta usar, por exemplo:
string userName = UserSessions[changeDescription.SessionId];

Testei esta solução no XP, 7 e 8.1 e funcionou perfeitamente.

Desculpem por não postar o código como um todo, mas a verdade que o meu código original é bem diferente do postado aqui, pois algumas coisas eu transformei em classes distintas para reusabilidade em outras partes do meu projeto. Mas a essência da minha solução é esta.

Desculpem também por postar os códigos como imagem, mas a formatação do Blogspot para a endentações e cores não ajuda muito.

Espero que minha solução sirva para mais alguém ou que alguém apresente algo mais "requintado". Comentários são sempre bem-vindos.

Até o próximo post.