8 jun 2011

Construyendo un servicio Windows

Este es un pequeño manual para construir servicios Windows, con recomendaciones de mi propia cosecha sin que esto se acerque a mejores prácticas o algo similar, simplemente anotaciones que para mi son útiles.
Primeramente creamos un proyecto de tipo C# -> Windows y usamos la plantilla Servicio Windows

Luego eliminamos la clase Service1.cs y elegimos agregar un nuevo elemento, y elegimos Servicio de Windows y ponemos el nombre que queremos, en mi caso igual al proyecto TestService

Una vez hecho esto vamos a la clase Program.cs que es de donde realmente se arranca el servicio y don de se instancia el servicio cambiamos Service1 por el nombre que hallamos elegido para nuestro servicio

static void Main()
{
	ServiceBase[] ServicesToRun;
	ServicesToRun = new ServiceBase[];
	{
  		new TestService()
	};
	ServiceBase.Run(ServicesToRun);
}

Ahora bien deseo que mi Servicio Windows tenga algunas llaves parametrizables e instrumentación. Entonces el siguiente paso es agregar un archivo de configuración. Al respecto recordar que el archivo de configuración debe llamarse igual que el servicio más las extensiones ”.exe.config“ o sea, que en nuestro ejemplo seria TestService.exe.config. Para lograr esto añadimos un nuevo elemento tipo Archivo de Configuración de Aplicación

Por cuestiones de requisitos, el webservice debe tener una hora de primera ejecución y luego un intervalo de tiempo. Para esto añadimos dos llaves en el archivo de configuración que acabamos de crear.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key ="HoraEjecucion" value="08:00:00"/>
<add key="FrecuenciaEjecucion" value="30"/>
</appSettings>
</configuration>

La primera hora de ejecución será a la 8 de la mañana y a partir de ahí se ejecutara cada 30 minutos. Ahora necesitamos utilizar estas llaves en nuestra aplicación. Lo primero a tener en cuenta es que si el servicio se inicia a las 5:00 todavía faltaría 3 horas para que el servicio comience a trabajar. Una de las formas de lograr esto es utilizando un Timer, leemos la llave del archivo de configuración las llaves, calculamos el tiempo que falta la primera ejecución, lo colocamos en el intervalo del mismo y lo enlazamos con el evento principal del servicio.

Primero necesitamos tener acceso a las llaves del archivo, entonces añadimos una referencia a la librería de .Net System.configuration y la utilizamos usando dentro de la clase del servicio utilizando el using


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Configuration;
namespace TestService
{
 partial class TestService : ServiceBase
 {
    System.Timers.Timer timer = new System.Timers.Timer();
    public TestService()
    {
        InitializeComponent();
        timer.Enabled = false;
        timer.Elapsed += (evento);
    }

    protected override void OnStart(string[] args)
    {
        PrimeraEjecucion();
    }

    protected override void OnStop()
    {
       /*TODO: agregar código aquí para realizar cualquier anulación necesaria para detener el servicio.*/
    }

    private void evento(object sender, System.Timers.ElapsedEventArgs e)
    {
        MetodoPrincipal();
    }

    protected void MetodoPrincipal()
    {
        timer.Enabled = false;
        /*TODO: Logica Principal del Servicio*/
        SiguienteEjecucion();
    }

    protected void PrimeraEjecucion()
    {
        TimeSpan HoraInicio = TimeSpan.Parse(ConfigurationManager.AppSettings["HoraEjecucion"]);

        TimeSpan TiempoEspera = (System.DateTime.Now.TimeOfDay.Subtract(HoraInicio).Ticks < 0) ?
                 System.DateTime.Now.TimeOfDay.Subtract(HoraInicio).Negate() :
                 HoraInicio.Add(new TimeSpan(1, 0, 0, 0)).Subtract(System.DateTime.Now.TimeOfDay);

        ActualizaTimer(TiempoEspera.TotalMilliseconds);
    }

    protected void SiguienteEjecucion()
    {
        double minutos = Convert.ToDouble(ConfigurationManager.AppSettings["FrecuenciaDeEjecucion"]);
        ActualizaTimer(TimeSpan.FromMinutes(minutos).TotalMilliseconds);
    }

    protected void ActualizaTimer(double milisegundos)
    {
        timer.Enabled = false;
        timer.Interval = milisegundos;
        timer.Enabled = true;
    }
 }
}

La explicación del código de arriba es la siguiente:

Primero creamos el timer de forma global. En la inicialización del componente deshabilitados el timer y, por medio del delegate, le asignamos un evento que apunta al método principal del webservices. Luego en el onStart del servicio llamamos al método PrimeraEjecucion Este se encarga de leer la llave correspondiente a la hora de la primera ejecución del archivo de configuración y calcular cuando hace falta para que esta se alcance (TiempoEspera), lo convierte a milisegundos y con esto llama al método ActualizaTimer que se encarga de añadir el tiempo al Timer y habilitarlo. Con esto conseguimos que una vez que se alcance el tiempo que falta para la primera ejecución el método principal del servicio se ejecute. Finalmente en el método Principal lo primero que hacemos es deshabilitar el timer para no incurrir en llamadas traslapadas si el tiempo de espera para la primera ejecución es muy corto, y al final del mismo llamamos al método SiguienteEjecucion que el intervalo de tiempo de las siguientes ejecuciones del método principal y se lo pasa a ActualizaTimer.

El método SiguienteEjecucion lo podemos modificar un poco de manera que por ejemplo reciba un boolean para saber si la siguiente ejecución es normal o no, previendo que si hay un error en la ejecución del método principal la siguiente ejecución sea en un tiempo distinto a que si terminara de modo normal. Es solo una idea que podríamos implementar.

Ahora queremos implementar la instrumentación, en su faceta de seguimiento o reporte de errores, básicamente habilitar el tracing. Hay varias formas de hacerlo, pero para este caso decidí hacerlo por medio del archivo de configuración usando System.Diagnostics.

Inmediatamente luego de cerrar la sección AppSetttings incluimos la siguiente sección

<system.diagnostics>
<trace autoflush ="true" />
<sources>
 <source name="SourcePrueba" switchName="SwicthPrueba" switchType="System.Diagnostics.SourceSwitch">
   <listeners>
     <clear/>
     <add name="evtlogPrueba"
       type="System.Diagnostics.EventLogTraceListener"
       initializeData="PruebaServicio" />
     <add name="textwriterListener"
       type="System.Diagnostics.TextWriterTraceListener"
       initializeData="Log_ServicioPrueba.txt"
       traceOutputOptions="ProcessId, DateTime, Callstack" />
   </listeners>
 </source>
</sources>
<switches>
 <add name="SwicthPrueba" value="Verbose"/>
</switches>
</system.diagnostics>

Con esto hacemos varias cosas autoflush significa que conforme registremos eventos inmediatamente se incluirán en sus respectivos destinos. El source será lo que instanciamos en la aplicación para implementar el tracing. Este source, a su vez, utiliza el switch especificado para determinar que se va a registrar; esto lo veremos más claramente más adelante. Además incluimos dos listeners uno que es un eventlog y otro que es un archivo de texto. En el que es de tipo eventlog el initializeData se refiere al source del eventlog. Finalmente definimos un switch el cual por, medio de su value, determinara que se registrará cuando usemos el TraceSource en la aplicación.

Quizás todo lo anterior es un poco confuso ya que es la mera configuración del tracing. Ahora lo veremos como utilizarlo dentro de nuestro servicio. Primeramente instanciamos un TraceSouce de manera global (en la misma área que instanciamos el timer)

TraceSource ts = new TraceSource("SourcePrueba");

Como vemos lo instanciamos utilizando como parámetro el mismo nombre que usamos para el source que definimos en la configuración. Ahora vamos ha utilizarlo en varias partes de nuestro código. Por ejemplo vamos a registrar un evento cuando el servicio se inicie y cuando se detenga, modificando los respectivos métodos de la siguiente manera.

protected override void OnStart(string[] args)
{
	ts.TraceEvent(TraceEventType.Information,0,"Servicio Test Iniciado");
	PrimeraEjecucion();
}

protected override void OnStop()
{
	ts.TraceEvent(TraceEventType.Information,0,"Servicio Test Finalizado");
}

El método principal lo vamos a modificar de la siguiente manera para probar como funciona el switch del archivo de configuración al final del presente manual.

protected void MetodoPrincipal()
{
	timer.Enabled = false;
	ts.TraceEvent(TraceEventType.Information, 0, "Servicio Test: Informacion");
	ts.TraceEvent(TraceEventType.Error, 0, "Servicio Test: Error");
	ts.TraceEvent(TraceEventType.Warning, 0, "Servicio Test: Alerta");
	ts.TraceEvent(TraceEventType.Verbose, 0, "Servicio Test; General/Siguimiento");
	/*TODO: Logica Principal del Servicio*/
	SiguienteEjecucion();
} 

Prácticamente hemos finalizado, la creación de nuestro servicio de Prueba. Sin embargo nos falta prepararlo para poder instalarlo. Primero necesitamos añadirle un instalador a nuestro proyecto. Vamos a la clase TestService.cs, pero no al código si no al diseñador, en éste hacemos clic derecho y elegimos Agregar Instalador.

Elegimos la nueva clase que se nos ha añadido ProjectInstaller.cs en su diseñador y escogemos el componente ServiceInstaller1 nos vamos a propiedades y cambiamos algunas propiedades

Como se aprecia, podemos cambiar la descripción del servicio y el nombre que mostrará en la consola de servicios de windows. Así como si quedará con activación automática o manual. Lo configuramos según nos parezca.

Finalmente debemos decidir con que cuenta se ejecutará el servicio para esto seleccionados el serviceProcessInstaller y en sus propiedades buscamos el elemento Account. Se seleccionamos según se desee (Por lo general los servicios se ejecutaran con LocalSystem)

Ahora vamos a crear dos archivos .bat para instalación y desinstalación de nuestro servicio. Esto es muy cómodo por ejemplo, cuando necesitamos pasarlo a otro departamento para su instalación. Estos archivos se puede crear utilizando notepad y luego de guardarlo simplemente le cambiamos la extensión de .txt a .bat

El archivo Instalador.bat quedaría como sigue:

@ECHO OFF

REM Estos es para usar con el Framework .NET 2.0

set DOTNETFX2=%SystemRoot%\Microsoft.NET\Framework\v2.0.50727

set PATH=%PATH%;%DOTNETFX2%

echo Instalando Servicio de Prueba...

echo ---------------------------------------------------

InstallUtil /i TestService.exe

echo ---------------------------------------------------

echo Instalacion Finalizada.

echo ---------------------------------------------------

pause

Y el Desinstalar.bat así:

@ECHO OFF

REM Estos es para usar con el Framework.NET 2.0

set DOTNETFX2=%SystemRoot%\Microsoft.NET\Framework\v2.0.50727

set PATH=%PATH%;%DOTNETFX2%

NET STOP "TestService"

echo Desinstalando Servicio de Prueba...

echo ---------------------------------------------------

InstallUtil /u TestService.exe

echo ---------------------------------------------------

echo Hecho

pause

Nos asegurarmos que todo compila, movemos los archivos que necesitamos a una carpeta aparte para realizar nuestras pruebas.

Comencemos ejecutando el archivo Instalador.bat . Si al seleccionar la cuenta del servicio le indicamos user en lugar de LocalSystem, por poner un ejemplo, probablemente nos pida el usuarioy su respectiva contraseña para ejecutar el servicio Windows en la máquina

Vamos a la consola de servicios para verificar que efectivamente se instaló el servicio.

Antes de iniciar el servicio debemos asegurarnos que si vamos a utilizar un eventlog personalizado este debe estar creado y asociado con el source que indicamos en el archivo de configuración previamente. Si no lo hacemos se creará el source, pero asociado con el eventlog default que suele ser el de aplicación. Una vez hecho esta verificación si fuese el caso, lo iniciamos.

Una vez iniciado, si queremos depurarlo lo que hacemos es asociar el proceso desde Visual Studio. Depurar -> Asociar el Proceso, eligiendo en la pantalla que se nos muestra el proceso en cuestión.

Una vez asociado ya podemos depurar

Una vez ejecutado el método principal del servicio verificamos que los eventos se registraron tanto en el archivo como en el eventlog:

Ahora bien, anteriormente no quedó muy claro para que sirve el switch que definimos en el archivo de configuración, pues bien en este momento se están registrando todo los eventos que definidos en el TraceSource porque este switch tiene el valor Verbose, que significa que registre todo, si queremos que únicamente los errores se registren, es tan simple como cambiar el valor del swicth en el archivo de configuración y reiniciar el Servicio, sin alterar en lo mas mínimo nuestra programación. Para probarlo realizamos el siguiente cambio en el archivo de configuración

<add name ="SwicthPrueba" value="Error"/>

Detenemos el servicio, borramos las entradas del eventlog y el archivo. Iniciamos de nuevo el servicio y procedemos a verificar que ahora únicamente se registran los errores.

Esto es todo de momento, hasta aquí este manual de cómo crear un servicio Windows.

Roy {aka. Foy}

Autor & Editor

Desarrallador y líder técnico, con experiencia en tecnologías Microsoft desde los tiempos del VB6 y el asp clásico hasta el .Net Core, pasando por COM+, javascript, angularjs, Ionic, xaml, cordova, MVC, Web Api, Sql Server, Oracle... . Ávido lector, apasionado programador.