Desenvolvimento - Visual Basic .NET

Desenvolvendo Serviços para o Windows

Os Windows Services, também conhecidos como NT Services, permitem-nos criar aplicações que rodam em “background” no sistema operacional, rodando em sua própria sessão no Windows...

por Wallace Cézar Sales dos Santos



Recursos necessários:

- Visual Studio .Net 2002 ou 2003
- Sistema Operacional Microsoft Windows XP ou qualquer versão do 2000 ou 2003.

Introdução

Os Windows Services, também conhecidos como NT Services, permitem-nos criar aplicações que rodam em "background" no sistema operacional, rodando em sua própria sessão no Windows. Estes serviços podem ser automaticamente inicializados quando o sistema operacional inicia sua atividade, podendo ainda ser pausado e reiniciado, sem apresentar nenhuma interface com o usuário. Desta forma, os "serviços" são ideais para ser usado em servidores ou em funcionalidades de longa duração que necessitem ser executadas de forma totalmente independente do usuário.

Um tipo de aplicação interessante para serviços é a criação de um servidor de aplicações, como por exemplo, um serviço de cache de dados, com acesso remoto. Para acompanhar nosso artigo, estaremos desenvolvendo um serviço responsável por "ouvir" um determinado diretório, enviando por email qualquer alteração ocorrida no mesmo. Chamaremos esta aplicação de DirectoryListener.

Entendendo serviços

A primeira coisa que precisamos saber é que os serviços não são aplicações comuns, possuindo características que nos obrigam a trabalhar de forma diferente. Porque as aplicações Windows Service são diferentes das demais aplicações? Vejamos:

  • Uma aplicação Windows Service não pode ser simplesmente executada. Ela precisa do ambiente do Windows Service Control Manager para poder realizar suas atividades. Essa necessidade se dá inclusive para busca de erros da aplicação, o chamado "debug". Para realizar essa tarefa, você precisa iniciar o serviço e então, anexar o processo do serviço ao debugger. As demais aplicações não necessitam desse ambiente de execução;

  • Diferente de alguns tipos de projetos, é obrigatório criar os componentes de instalação para aplicações Windows Service. Essa tarefa é realizada pelas classes ServiceProcessInstaller e ServiceInstaller, que possuem os serviços necessários para realizar a instalação e registro do serviço desenvolvido. Esta operação é transparente, não requerendo nenhum processo especial, pois ela é invocada pelo utilitário de linha de comando InstallUtil.exe, que é o responsável, como já falamos, pela instalação de nosso serviço. Basicamente estas duas classes realizam todas as entradas de registro necessárias para que nosso serviço ser reconhecido e gerenciado pelo WSCM;

  • O metodo Main existente no serviço precisa obrigatóriamente executar o comando Run para que, como já falamos, adicionarmos nosso serviço sob o gerenciamento do WSCM. Não esqueça: Carregar o serviço não é o mesmo que iniciar o serviço. Já explicamos esta diferença;

  • Como nosso serviço executa em outro processo, independente do usuário que está logado, e não possue interação, impossibilitando assim que janelas de diálogo sejam exibidas a partir de um serviço, podendo inclusive, impedir a execução do serviço se houver tentativas nesse sentido. Desta forma, mensagens de erro devem ser direcionadas para o log de eventos do windows ao invés de direcionadas para a User Interface;

  • Os serviços executam no seu próprio contexto de segurança e são iniciados antes do usuário realizar o seu registro no sistema. Por isso, deve-se planejar cuidadosamente cenários onde o serviço irá executar.

Entendidas as diferenças, vamos então trabalhar na criação de um serviço.

Criando um projeto de serviço Windows

Primeiro vamos criar o projeto: no Visual Studio .NET, selecione o menu File " New " Project... ou tecle "Ctr+N". Na lista "Project Types:", selecione "Visual Basic Projects" e na lista "Templates", selecione o modelo "Windows Service". Selecione o diretório onde deseja criar a aplicação e clique OK.

A aplicação criada possue uma classe, chamada Service1 (renomeie-a para DirectoryListener), que implementa (herda) a classe ServiceProcess.ServiceBase, definindo assim a classe de serviço para nossa aplicação. Na janela "Solution Explorer", selecione a o arquivo Service1.vb e renomeie-o para DirectoryListener.vb.

A enumeração System.ServiceProcess.ServiceType define os vários tipos de serviços existentes no Windows. É utilizada como propriedade ReadOnly da classe ServiceController. Porém somente dois tipos de serviços são possíveis de desenvolver com a plataforma .NET:

  • Win32OwnProcess - são serviços que possuem um único processo para sua execução; e
  • Win32ShareProcess - são serviços que compartilham recursos na sua execução.

O tipo de serviço indica como o serviço é utilizado pelo sistema. Ele representa "flags" combinadas, usando o operador "Bitwise" OR.

Não podemos desenvolver serviços que permitem interatividade com o usuário. A interação com nossas aplicações é realizada através do uso de Sockets ou Remoting. Mas nosso serviço rodará "sozinho". Na Listagem 1 apresentamos os namespaces utilizados no desenvolvimento de nosso serviço.

Listagem 1: Namespaces utilizados no serviço DirectoryListener

Imports System.ServiceProcess
Imports System.Configuration
Imports System.IO
Imports System.Web.Mail

O namespace System.ServiceProcess é o nativo dos serviços. Os demais estaremos utilizando respectivamente para acessar as configurações do serviço, armazenadas no arquivo DirectoryListener.exe.Config, para realizar a "escuta" do diretório determinado (principal objetivo do nosso serviço) e, para enviar os emails.

Adicionemos também um objeto do tipo FileSystemWatcher(), que será o responsável pelas atividades principais de nosso serviço: Private _watcher as FileSystemWatcher. Como configurar a segurança

Windows Service sempre executam dentro de um contexto de segurança, manipulando a propriedade Account para o processo para o qual o serviço está sendo executado. Esta propriedade pemite-nos definir um dos quatro tipos de contas para contexto de segurança:

  • User - a qual solicitará um nome de usuário e senha válidos para quando o serviço é instalado e executado no contexto de uma conta especificada na rede;
  • LocalService - a qual executará no contexto de uma conta que age como um usuário não privilegiado no computador local e com credenciais anônimas em qualquer computador remoto;
  • LocalSystem - a qual executará o contexto de uma conta que possue privilégios locais e possui credenciais em qualquer computador remoto;
  • NetworkService - a qual executará no contexto de uma conta que age sem privilégios no computador local e possue credenciais em qualquer computador remoto.

A propriedade Account pode ser configurada no objeto ServiceInstaller que deve acompanhar todos os nossos projetos Windows Service. Esta propriedade define um tipo ServiceAccount que define o contexto de segurança para instalação. De todos os tipos aqui demonstrados, fica-nos claro que o contexto LocalSystem é o que possue mais privilégios. Porém, a grande maioria dos serviços não necessita deste grau de prinvilégio e como falamos de segurança, devemos sempre procurar fornecê-la na medida da necessidade. Para fins didáticos, determinaremos para nosso serviço a Account tipo LocalSystem. Para configurar, devemos:

1. Na janela "Solution Explorer", selecione o arquivo DirectoryListener.vb e clique no menu View " Designer, ou tecle "Shift+F7";

2. Na janela DirectoryListener.vb[Design] aberta, clique no menu View " "Properties Window", ou tecle "F4", abrindo a janela Properties;

3. Na parte inferior da janela Properties você visualizará o link Add Installer. Clique nele. Será adicionado em seu projeto um arquivo chamado ProjectInstaller.vb, contendo dois objetos: ServiceProcessInstaller1 e ServiceInstaller, responsáveis pela instalação do executável e classes do nosso serviço, sendo acessada pelo utilitário InstallUtil.exe (Veja na seção Instalando um Serviço). Renomeie os objetos para DirectoryServiceProcessInstaller e DirectoryServiceInstaller, respectivamente;

4. Na janela "Solution Explorer", selecione o arquivo ProcessInstaller.vb e clique no menu View " Designer, ou tecle "Shift+F7";

5. Na janela ProjectInstaller.vb[Design] aberta, selecione o objeto DirectoryServiceProcessInstaller e clique no menu View " "Properties Window", ou tecle "F4", abrindo a janela Properties;

6. Selecione a propriedade Account e altere seu valor para LocalSystem.

Fazendo o serviço funcionar: trabalhando com eventos

Os Windows Service possuem por padrão alguns comportamentos (traduzidos por métodos), que são derivados da classe pai, System.ServiceBase, e que sobreescrevendo, ou no jargão do .NET "Override", determinamos o que acontecerá quando:

  • O serviço iniciar : OnStart
  • O serviço é pausado: OnPause
  • O serviço é parado: OnStop
  • O serviço é reiniciado após ter sido pausado: OnContinue
  • Ocorre um "shutdown" no sistema, se o serviço estiver rodando naquele momento: OnShutDown
  • O serviço receber um comando customizado: OnCustomCommand
  • Um evento do gerenciador de energia for recebido, tais como bateria baixa ou suspensão de operações: OnPowerEvent

Estes métodos representam estados em que o serviço moveu-se no seu ciclo de vida, ou seja, as transições de um estado para o seguinte. Nesse sentido, existe uma certa hierarquia que precisa ser respeitada, como por exemplo o fato do serviço não responder ao evento OnContinue se o evento OnStart não tive já ocorrido.

Além disso, vinculados aos métodos, nossos serviços possuem propriedades que determinam se estes eventos serão ou não executados no nosso serviço. São as propriedades CanStop, para o método OnStop, CanPauseandContinue para os métodos OnPause e OnContinue, CanShutDown para o método OnShutDown e CanHandledPowerEvent para o método OnPowerEvent. Para permitirem a execução, estas propriedades devem estar configuradas para true. Com o arquivo DirectoryListener.vb aberto em modo de design, observe as propriedades do mesmo. Note também que para o método OnStart não temos uma propriedade. Isto ocorre porque sua implementação é obrigatória, pois é nele que iniciamos as atividades em nosso serviço. Além deste, é fundamental que implementemos também o método OnStop, pois determinamos o que ocorrerá se nosso serviço for parado.

Como são propriedades do serviço, podem ser acessadas diretamente na janela de propriedade do Visual Studio .NET:

1. Na janela "Solution Explorer", selecione o arquivo DirectoryListener.vb e clique no menu View " Designer, ou tecle "Shift+F7";

2. Na janela DirectoryListener.vb[Design] aberta, clique no menu View " "Properties Window", ou tecle "F4", abrindo a janela Properties;

3. Verifique as propriedades citadas. Note que o valor padrão de todas é "False", com excessão da propriedade CanStop, que possue o valor True.

Aproveite também e observe a propriedade AutoLog. Esta propriedade, que possue o valor padrão True, premitindo assim que nosso serviço automaticamente interaja e registre no Application Event Log (que pode ser acessado pelo Event Viewer do Windows), escrenvendo informações sobre os estados do serviço, tais como o início e o fim (start e stop) do serviço, e as excessões ocorridas durante o ciclo de vida do serviço.

No nosso serviço, estaremos apenas implementando os dois métodos principais: OnStart e OnStop, conforme podemos verificar na Listagem 2. Estes serviços chamam as procedures ActivateListener(ByVal path as String) e DeActivateListener(), que são responsáveis, respectivamente, por configurar e criar nosso objeto FileSystemWatcher (variável _watcher declarada). Não entraremos em detalhes sobre o classe FileSystemWatcher por não ser o escopo deste artigo, porém o código possue os comentários necessários para compreender a implementação. Listagem 2: Implementação dos métodos OnStart e OnStop no serviço DirectoryListener

Protected Overrides Sub OnStart(ByVal args() As String)
    " Add code here to start your service. This method should set things
    " in motion so your service can do its work.
    Me.ActivateListener(ConfigurationSettings.AppSettings("pathToListen"))
End Sub

Protected Overrides Sub OnStop()
    " Add code here to perform any tear-down necessary to stop your service.
    Me.DeActivateListener()
End Sub

"Activate the FileSystenWatcher Object
Private Sub ActivateListener(ByVal path As String)
    Me._watcher = New FileSystemWatcher(path)
    " Watch for changes in LastAccess and LastWrite times, and
    " the renaming of files or directories. 
    With Me._watcher
       .NotifyFilter = (NotifyFilters.Size Or NotifyFilters.LastAccess Or NotifyFilters.LastWrite Or _
 		    NotifyFilters.FileName Or NotifyFilters.DirectoryName)
       "Include Sub directories of main directory
       .IncludeSubdirectories = True
       " All files.
       .Filter = ""
       " Add event handlers.
       AddHandler .Changed, AddressOf OnChanged
       AddHandler .Created, AddressOf OnChanged
       AddHandler .Deleted, AddressOf OnChanged
       AddHandler .Renamed, AddressOf OnRenamed
       " Begin watching.
       .EnableRaisingEvents = True
    End With
End Sub

"Disable the FileSystenWatcher Object
Private Sub DeActivateListener()
    Me._watcher.Dispose()
    Me._watcher = Nothing
End Sub

Um ponto importante: o método OnStart é o ponto inicial de nosso serviço porque é nele que colocamos todas as ações que devem ocorrer quando o serviço é inicializado. Não devemos colocar ações relativas ao serviço a ser executado no construtor da classe, por um motivo muito simples: o construtor é executado quando o executável roda, e não quando o serviço é iniciado. O executável roda antes do OnStart. Assim, se o serviço é parado, quando ele é novamente executado, o contrutor não é mais acionado, pois o serviço já se encontra na memória do Windows Service Control Manager (WSCM).

Um outro método da classe base que é executado é o método Run, que pode ser observado no método Main() inserido na região " Component Designer generated code ". Este método é o responsável por colocar nosso serviço na memória do WSCM.

Instalando um serviço

Diferente dos outros projetos .NET, um Windows Service não é instalado apenas copiando o seu assembly principal (arquivo .exe) para um determinado diretório e, com um duplo clique, estará sendo executado.

Sua instalação depende, basicamente, de duas partes: a primeira, já brevemente descrita na seção Como configurar a segurança, onde tivemos de adicionar três classes de instalação em nosso projeto: ProjectInstaller, DirectoryServiceProcessInstaller e DirectoryServiceInstaller. Essas classes serão responsáveis por realizar os registros necessários do nosso serviço para que o mesmo possa ser executado e gerenciado pelo WSCM. Antes que você se pergunte se entendeu corretamente, eu adianto: sim, registros! Logo vem o questionamento: Com .NET não paramos de registrar nossos componentes? Sim, paramos... mas isso não se aplica aos Windows Services (existem algumas outras excessões, mas não é o escopo deste artigo).

Para que possamos apresentar do gerenciador de serviços (menu do windows Administrative tools " Services) uma descrição do nosso serviço, como vemos na Figura 1, precisamos sobrescrever dois métodos da classe ProjectInstaller: Install e Uninstall. Esta classe herda a classe System.Configuration.Install.Installer, que é uma classe existente no .Net Framework utilizada na interação com instalações customizadas que desejemos realizar, como no caso de nosso serviço.


Figura 1: Visualização do serviço, após instalado, no gerenciador de serviços do Windows

Nos métodos Install e Uninstall estaremos adicionando os comandos necessários para inserir no Registry do Windows a descrição do nosso serviço, conforme vimos na Figura 1. Na Listagem 3 observamos as implementações dos métodos citados. No primeiro método, inserimos os comandos necessários para abrir as chaves do Registry: System, CurrentControlSet, Services, DirectoryListener e adicionar a descrição de nossa classe: "Windows Service that notify selected user with email messages with changes in a defined directory. If the service is stopped, the users will not receive this messages.". O segundo método realizamos a tarefa de apagar a descrição inserida. Existe uma observação importante sobre a realização da implementação dos métodos Install e Uninstall. Observando a Listagem 3, notamos que o primeiro comando adicionado é a chamada à execução na classe base dos respectivos métodos. Isto é necessário, porque na classe pai é executada a chamada a todos os instaladores listados na propriedade Installers (coleção InstallerCollection), realizando assim a correta instação/desinstalação do componente.

Por fim, precisamos executar a instalação e esta tarefa é realizada através do utilitário InstallUtil.exe, disponibilizado no .Net framework SDK. Basicamente, esta ferramenta permite-nos instalar e desintalar recursos servidores (serviços), através da execução dos componentes de instalação de um assembly especificado, e é por isso que adicionamos as classes acima citadas.

Listagem 3: Sobrescrevendo os métodos Install e Uninstall

Public Overrides Sub Install(ByVal stateSaver As System.Collections.IDictionary)
    Dim system, currentControlSet, services, service, config As Microsoft.Win32.RegistryKey
    Try
        MyBase.Install(stateSaver)
        system = Microsoft.Win32.Registry.LocalMachine.OpenSubKey("System")
        currentControlSet = system.OpenSubKey("CurrentControlSet")
        services = currentControlSet.OpenSubKey("Services")
        service = services.OpenSubKey(Me.DirectoryServiceInstaller.ServiceName, True)
        service.SetValue("Description", "Windows Service that notify selected user with email messages 
with changes in a defined directory. If the service is stopped, the users will not receive this 
messages.")
        config = service.CreateSubKey("Parameters")
    Catch ex As Exception
        Me.RegisterLog(ex.Message)
    End Try
End Sub

Public Overrides Sub Uninstall(ByVal savedState As System.Collections.IDictionary)
    Dim system, currentControlSet, services, service, config As Microsoft.Win32.RegistryKey
    Try
        MyBase.Install(savedState)
        system = Microsoft.Win32.Registry.LocalMachine.OpenSubKey("System")
        currentControlSet = system.OpenSubKey("CurrentControlSet")
        services = currentControlSet.OpenSubKey("Services")
        service = services.OpenSubKey(Me.DirectoryServiceInstaller.ServiceName, True)
        service.DeleteSubKey("Parameters")
    Catch ex As Exception
        Me.RegisterLog(ex.Message)
    End Try
End Sub

A tarefa é relativamente simples. Após finalizado o projeto, realize o build do serviço. Para isso, selecione e clique no menu Build " Build DirectoryListenerService. Se você não realizou mudanças na estrutura de geração do seu Visual Studio .NET, você terá criado no diretório Bin de seu projeto o arquivo DirectoryListener.exe (nome que definimos para nosso assembly). Realizado o build, abra a ferramenta Visual Studio .NET 2003 Command Prompt (Menu Start " All Programs " Visual Studio .NET 2003 " Visual Studio .NET Tools). Aqui temos duas opções: ir, através da linha de comando até o diretório onde foi gerado o assembly (diretório Bin) ou determinar seu caminho completo para utilizar no comando que veremos a seguir.

Para realizar a instalação, executamos seguinte linha de comando (se estivermos no diretório Bin): installutil "DirectoryListener.exe" (se não estiver no diretório Bin, adicione anteriormente ao nome do assembly o caminho completo). Se precisar desinstalar o serviço, execute a seguinte linha de comando: installutil /u "DirectoryListener.exe". O "/u" é o comando interno na ferramenta apontando para a tarefa de desinstalar. Existem outras opções de execução da ferramenta. Para visualizá-las, digite installutil /?. Uma vez realizada esta tarefa, basta ir ao gerenciador de serviços do Windows, procurar e selecionar nosso serviço e clicar em Start.

Corrigindo erros: trabalhando com o Debugger

Um ponto que é realmente negativo nos projetos Windows Service é a questão da identificação e correção de erros. É um processo quase artesanal e, posso até dizer, cansativo. Isto porque um serviço precisa interagir com o WSCM para executar. Assim, você precisa instalá-lo, para então poder realizar a depuração do seu código.

Assim sendo, após instalar o serviço, espere aproximadamente 30 segundos. Se o houve algum erro na inicialização do serviço serviço, o gerenciador de serviços do Windows apresentará uma mensagem informando que ocorreu um erro. Se a propriedade AutoLog estiver configurada para True,este erro será registrado no Event Viewer do Windows.

A documentação do .NET Framework SDK cita que no mesmo acima citado, se adicionado um "break-point" no método OnStart, o mesmo irá permitir a depuração passo-a-passo do serviço. Eu confesso que não consegui realizar esta proeza, necessitando, quando ocorre erro nos meus projetos de serviço, adicionar vários pontos de registro no log do windows, determinando por onde meu código está passando e identificando as excessões que ocorrem.

Conclusão

Como vimos, com .NET a tarefa de criar serviços para serem executados no Windows ficou muito simples. Neste artigo procuramos passar uma visão completa de como criar estes serviços. É claro que nosso exemplo é simples, mas o objetivo é focarmos nos serviços e em sua importância dentro do contexto de nossas soluções.

Deixo para você leitor um estudo de suas necessidades, e assim, a possibilidade de identificar dentro de seus requisitos de sistema, onde este artigo poderá ajudár no seu dia-a-dia.

Nos códigos demostrados aqui, não trazemos todos os detalhes da implementação. Para isso, solicito que realizem o download do projeto (download aqui), procurando verificar todos os detalhes de como implementamos este serviço. Um detalhe importante: como comentamos no início deste artigo, nosso serviço utiliza um arquivo ".config". Este arquivo deve ser adicionado no mesmo diretório onde está localizado o assembly (.exe).

* Este artigo é de propriedade do autor, não sendo permitida qualquer publicação, cópia ou alteração de seu conteúdo ou parte dele, sem a prévia autorização deste.

Wallace Cézar Sales dos Santos

Wallace Cézar Sales dos Santos - Arquiteto de Software da Datasul S.A., responsável pelos sistemas que utilizam a tecnologia Microsoft .NET. Profissional certificado como MCSD (Microsoft Certified Solution Developer), MCP (Microsoft Certified Professional) e MVP (Microsoft Most Valuable Professional) e membro do INETA Speaker Boreau, sendo contribuidor de diversas comunidades de desenvolvedores nacionais que utilizam a tecnologia .Net. É também co-autor do livro Desenvolvendo com C#, lançado pela editora Bookman e está atualmente trabalhando num livro de Visual Basic .Net. Ele pode ser contactado por email ou MS Messenger.