.NET

18 fev, 2020

Criando um Windows Service usando TopShelf e Timer em .NET Core

Publicidade

Hoje em dia, existem diversas soluções para desenvolver um Windows Service que execute de tempo em tempo, cada uma com sua peculiaridade e maneira de configurar.

Pensando na simplicidade de desenvolvimento e facilidade de instalação, selecionamos alguns recursos que juntos formam uma espécie de canivete suíço para esse tipo de aplicação: TopShelf e Timer em .NET Core 2.2 / 3.0.

Iremos criar, configurar e instalar em nossa máquina um pequeno projeto de testes utilizando esses recursos.

imagem enviada pelo autor

Criando a aplicação de testes

O projeto base que usaremos será um Console Application baseado no .NET Core 3.0 ou 2.2. Para esse exemplo, iremos escolher a versão 3.0, a mais recente.

Para configurar um projeto seguindo essas especificações, inicie seu Visual Studio, vá em em Create a new Project, selecione a opção Console App (.NET Core).

Na próxima tela, você poderá especificar o nome do projeto, bem como o seu diretório. Após concluir a configuração, teremos o projeto pronto para iniciarmos o desenvolvimento!

Na próxima tela, você poderá especificar o nome do projeto, bem como o seu diretório. Após concluir a configuração, teremos o projeto pronto para iniciarmos o desenvolvimento!

Instalando o TopShelf

Através do Manage NuGet Packages, iremos adicionar as dependências do TopShelf no nosso projeto:

A versão escolhida foi a 4.2.1, a última estável até então!

Criando classe de controle do serviço

Após concluirmos a instalação, iremos criar uma nova pasta no projeto principal chamada Infrastructure e adicionar uma nova classe responsável por controlar o início do serviço TopShelf. A chamaremos de ServiceBaseLifetime.

  • ·        Infrastructure
      •  ServiceBaseLifetime.cs

A ServiceBaseLifetime implementará a interface ServiceControl do TopShelf, que nos indica utilizar dois novos métodos que darão vida ao nosso serviço: Start e Stop, ambos recebendo como parâmetro o HostControl que possibilita ajustar alguns detalhes do host, falaremos mais sobre ele em outra oportunidade.

Como o próprio nome já sugere, o método Start será chamado toda vez que o Windows Service for inicializado pelo sistema operacional, enquanto que o Stop, toda vez que for parado.

Veja como ficará a implementação da ServiceBaseLifetime após implementarmos a interface:

using System;

using Topshelf;

namespace ProjetoTesteTopShelf.Infrastructure

{

                 public class ServiceBaseLifetime: ServiceControl

                {

                               private async Task ExecuteAsync()

                               {

                               }

 

                               public bool Stop(HostControl hostControl)

                              {

                              }

                 }

}

Configurando o Timer

Caso quiséssemos que nosso Windows Service executasse apenas uma vez, poderíamos simplesmente implementar as funções do nosso serviço dentro da classe Start, mas a ideia não era termos uma lógica que executasse de tempo em tempo?

Para isso, precisaremos configurar algum tipo de recurso que controle essa execução. O  escolhido da vez é o Timer (System.Threading.Timer)!

A ideia é utilizarmos o método Start para instanciar um novo Timer, ou seja, toda vez que o Windows Service for inicializado, ele irá configurar um novo Timer que receberá a frequência com que ele deve executar as funções e outros detalhes.

Para organizarmos melhor nossa classe, criaremos um novo método chamado ExecuteAsync! Ele que será chamado pelo Timer e guardará o que deve ser executado de tempo em tempo.

Já vamos aproveitar e retornar true no método Start e Stop, indicando que o TopShelf poderá iniciar ou parar o Windows Service, respectivamente.

Veja um exemplo, onde configuramos para o serviço executar a cada 60 segundos:

using System;

using System.Threading;

using System.Threading.Tasks;

using Topshelf;

namespace ProjetoTesteTopShelf.Infrastructure

{

               public class ServiceBaseLifetime: ServiceControl

               {

                              private Timer _timer;

                              public bool Start(HostControl hostControl)

                             {

                                             _timer = new Timer(callback: async c => await ExecuteAsync(),

                                               //State: Objeto que carrega informações que podem ser utilizadas      pelo método de callback (ExecuteAsync)

                                             state: null,

                                               //dueTime: Delay para inicializar o ExecuteAsync

                                               dueTime: TimeSpan.Zero,

                                               //Period: Frequência que o ExecuteAsync deverá ser executado

                                               period: TimeSpan.FromSeconds(60));

 

                                               return true;

                              }

 

                                private async Task ExecuteAsync()

                               {

                                              //Execução da lógica aqui

                              }

 

                             public bool Stop(HostControl hostControl)

                                          {

                            return true;

                         

                     }

 

                     private async Task ExecuteAsync()

                    {

                                      //Execução da lógica aqui

                   }

 

                   public bool Stop(HostControl hostControl)

                   {

                                  return true;

                   }

        }

}

 

 Percebeu que o método é assíncrono, não é mesmo? Pois é, isso nos possibilita utilizarmos async/await na execução.

Evitando execuções concorrentes (Opcional)

Se continuarmos o desenvolvimento mantendo a classe ServiceBaseLifetime do jeito que esta, tudo indica que funcionará, certo? Sim! Mas tem um detalhe importante que já vamos nos previnir aqui: concorrência!

Da maneira como está desenvolvido, se uma das execuções demorar mais do que 60 segundos, a segunda inicializará enquanto a primeira ainda é executada.

E se quisermos que a seguinte só inicie quando a anterior finalizar? Podemos fazer isso da seguinte maneira: Toda vez que uma execução inicializar, alteraremos a frequência do Timer para infinito, e quando finalizar, voltaremos para a frequência de 60 em 60 segundos:

private async Task ExecuteAsync()

              {

         try

         {

                                 _timer.Change(Timeout.Infinite, Timeout.Infinite);

                                   //Execução da lógica aqui

         }

         finally

         {

                           _timer.Change(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));

         }

     }

Chamando nosso ServiceBaseLifetime

O último passo agora é configurar a classe Program para chamar nosso ServiceBaseLifetime.

Iniciaremos alterando a declaração do método Main para ser assíncrono:

public static async Task Main(string[] args)

Agora, iremos criar um Host para conseguirmos injetar a dependência do ServiceBaseLifetime. Para isso, precisaremos instalar via Manage NuGet Package o Microsoft.Extensions.Hosting e utilizar o seu HostBuilder. Ele deve ser instanciado da seguinte maneira, já configurando o ServiceBaseLifetime:

var host = new HostBuilder()

                .UseContentRoot(Directory.GetCurrentDirectory())

                .ConfigureServices((hostBuilderContext, services) =>

                            {

                                    services.AddSingleton(typeof(ServiceBaseLifetime));

                            });

Logo depois, utilizando o HostFactory, iremos configurar o TopShelf para chamar os métodos Start e Stop do ServiceBaseLifetime.

Também já iremos aproveitar para configurar o reinício automático do serviço caso aconteça  algum problema. Além disso, já iremos definir os nomes e as descrições que aparecerão nos serviços do Windows. Veja a implementação completa dessa classe:

using System.IO;

using System.Threading.Tasks;

using Microsoft.Extensions.DependencyInjection;

using Microsoft.Extensions.Hosting;

using ProjetoTesteTopShelf.Infrastructure;

using Topshelf;

namespace ProjetoTesteTopShelf

{

               public class Program

               {

                             public static async Task Main(string[] args)

                             {

                                            var host = new HostBuilder()

                                                             .UseContentRoot(Directory.GetCurrentDirectory())

                                                             .ConfigureServices((hostBuilderContext, services) =>

                                                             {

                                                                   services.AddSingleton(typeof(ServiceBaseLifetime));

                                                             });

 

                                         HostFactory.Run(x =>

                                         {

                                              x.Service<ServiceBaseLifetime>(sc =>

                                              {

                                                 sc.ConstructUsing(s =>

                         host.Build().Services.GetRequiredService<ServiceBaseLifetime>());

                                                sc.WhenStarted((s, c) => s.Start(c));

                                                sc.WhenStopped((s, c) => s.Stop(c));

                                            });

 

                                            x.RunAsLocalSystem()

                                              .DependsOnEventLog()

                                             .StartAutomatically()

                                             .EnableServiceRecovery(rc => rc.RestartService(1));

 

                                            x.SetDescription(“Testes do TopShelf”);

                                            x.SetDisplayName(“Projeto Teste TopShelf”);

                                            x.SetServiceName(“ProjetoTeste.TopShelf”);

                                       });

 

                                        await Task.CompletedTask;

                          }

            }

}

 

Testando o projeto

Ao executar em modo debug, se colocarmos um breakpoint dentro do método ExecuteAsync, poderemos ver que teremos a execução de minuto a minuto ocorrendo normalmente.

 

Instalando o serviço

Podemos também instalar o serviço localmente. Para isso, iremos publicar (botão direito no projeto, selecione Publish) o nosso serviço com as seguintes configurações:

Através do PowerShell como Administrador, abriremos a pasta onde ele publicou o nosso serviço. Nela, teremos os seguintes arquivos gerados pela nossa publicação:

Podemos instalar o serviço executando: ./{nome do projeto}.exe install

Podemos agora abrir os Serviços do Windows e ver ele na lista, pronto para ser inicializado:

Caso queira desinstalar o serviço, execute o comando  ./{nome do projeto}.exe uninstall

Encerramos aqui este tutorial de como desenvolver, configurar e instalar um Windows Service para rodar de tempo em tempo! 

Escolhemos uma de diversas possibilidades para fazer isso. Conhece outra e gostaria de compartilhar com a gente? Tem alguma sugestão? Comente e colabore com a comunidade! =)

Código fonte do projeto: https://github.com/lucastedeschi/windowsservice-topshelf-timer

Abraço!