Servicios de Windows |
Publicado: 05/Feb/2003 Actualizado: 05/Feb/2003
|
Empecemos viendo la utilidad que nos permita "manejar" los Servicios de Windows y después crearemos uno que comprobará si una determinada aplicación está ejecutándose y de no ser así, poder iniciarla e incluso detenerla. A ese servicio le podremos pasar parámetros para que inicie o "inspeccione" el estado del ejecutable o proceso que le indiquemos.
Administrar los Servicios de Windows
Como te acabo de comentar, vamos a crear una pequeña utilidad en la que mostraremos los Servicios de Windows que estén actualmente registrados en nuestro equipo o en cualquiera de los equipos a los que tengamos acceso.
Mostraremos la información sobre los servicios, así como el estado actual en que se encuentra: si está iniciado o no. Mediante una serie de botones podremos iniciarlos, pausarlos, detenerlos, etc. Dependiendo de que podamos realizar esas acciones, los botones estarán habilitados o deshabilitados.La siguiente figura muestra el aspecto del formulario en tiempo de diseño:
Figura 1, La utilidad para administrar los Servicios de Windows en tiempo de diseñoComo sabrás, la administración de los Servicios de Windows podemos hacerla desde Servicios en Herramientas Administrativas, desde allí podemos iniciar, detener, pausar, etc. cualquiera de los servicios que estén configurados en el sistema, con nuestra utilidad podremos hacer todas esas cosas, o al menos lo intentaremos. No pienses que esta utilidad suplantará a la incluida en el sistema, simplemente nos servirá, para entre otras cosas, saber cómo usar una de las clases incluidas en el .NET Framework para "interactuar" con los Servicios de Windows: la clase ServiceController.
Nota:
El código mostrado está realizado en Visual Basic .NET, pero en el ZIP con el código completo, también se incluye la versión de C#, por si es ese tu lenguaje preferido.Como podemos ver en la figura 1, el formulario tendrá un control ListView (serviciosLv) que mostrará los servicios que actualmente estén instalados en el equipo o bien en el equipo indicado en la caja de textos correspondiente. Una vez que seleccionemos uno de los servicios de la lista, se actualizarán los botones de forma que sepamos "visualmente" las acciones que podemos realizar con dicho servicio. En la columna Estado del ListView se indicará si está funcionando, parado o en pausa. Es posible que algunos de los servicios que estén en ejecución no permitan que se detengan o se pasen a un estado de pausa, en esos casos los botones correspondientes no estarán habilitados.
Vamos a ver ahora el código usado para esta utilidad, en el cual usaremos sólo la clase ServiceController; para poder usar esta clase, debemos añadir a las referencias del proyecto una referencia a la librería System.ServiceProcess.dll y una importación de ese espacio de nombres para evitarnos tener que escribirla en cada una de las clases y enumeraciones que usaremos.
Option Strict On Imports System.ServiceProcess Public Class fAdministrarServicios Inherits System.Windows.Forms.Form ' '... ' Private servicios() As ServiceController Private servicio As ServiceController Private itemActual As ListViewItem 'En este código además de la importación del espacio de nombres en el que reside la clase ServiceController y de la declaración de la clase del formulario, declaramos un array o matriz en el que se asignarán los servicios instalados en el equipo a inspeccionar, una variable de ese mismo tipo que representará al servicio que hayamos seleccionado del ListView y dado que el estado del servicio puede cambiar al pulsar en alguno de los botones, usaremos también una variable que contenga el elemento del ListView actualmente seleccionado.
Para llenar el array de servicios, usaremos el método GetServices de la clase ServiceController, esta propiedad devuelve un array con los servicios del equipo indicado o del equipo actual si no se indicara ninguno o se utiliza la cadena ".", la cual indicará a ese método que nuestra intención es obtener los servicios del equipo actual.
Veamos el código del procedimiento que se encarga de llenar ese array y añadir los elementos correspondientes al ListView.Private Sub mostrarServicios() mostrarServicios("") End Sub Private Sub mostrarServicios(ByVal equipo As String) Dim i As Integer Dim lvi As ListViewItem ' If equipo = "" Then equipo = "." ' servicios = ServiceController.GetServices(equipo) serviciosLv.Items.Clear() For i = 0 To servicios.Length - 1 lvi = serviciosLv.Items.Add(servicios(i).ServiceName) lvi.SubItems.Add(servicios(i).Status.ToString) lvi.SubItems.Add(servicios(i).DisplayName) lvi.SubItems.Add(servicios(i).ServiceType.ToString) lvi.SubItems.Add(servicios(i).MachineName) ' ' guardar una referencia del servicio en la propiedad Tag lvi.Tag = servicios(i) Next End SubEl procedimiento permite dos formas de usarlo: indicando el nombre del equipo o sin ningún parámetro, en caso de que ese parámetro sea una cadena vacía utilizamos "." para indicar el equipo actual.
A continuación recorremos el array con los servicios devueltos por la función GetServices y asignamos los elementos (items) del ListView.
A la propiedad Tag del elemento actual le asignamos una referencia al servicio, para poder usarlo posteriormente al seleccionar un elemento en el ListView.Veamos a continuación el evento Load del formulario en el que se asignan las cabeceras del control ListView y se llama al procedimiento mostrarServicios para llenar la lista con los servicios del equipo en el que se ejecuta esta utilidad.
Private Sub Form_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load With serviciosLv .View = View.Details .FullRowSelect = True .GridLines = True .LabelEdit = False .HideSelection = False .Columns.Clear() .Columns.Add("Servicio", 100, HorizontalAlignment.Left) .Columns.Add("Estado", 60, HorizontalAlignment.Left) .Columns.Add("Descripción", 200, HorizontalAlignment.Left) .Columns.Add("Tipo de servicio", 100, HorizontalAlignment.Left) .Columns.Add("Equipo", 70, HorizontalAlignment.Left) End With ' iniciarBtn.Enabled = False iniciarParametrosBtn.Enabled = False pausarBtn.Enabled = False detenerBtn.Enabled = False continuarBtn.Enabled = False reiniciarBtn.Enabled = False ' equipoTxt.Text = "" parametrosTxt.Text = "" ' mostrarServicios() End SubEn este evento asignamos las características del control ListView, además de crear las cabeceras del mismo, las cuales eliminamos por si en tiempo de diseño se han creado algunas.
Deshabilitamos los botones, asignamos valores nulos a las dos cajas de textos y llamamos al procedimiento mostrarServicios el cual se encargará de asignar al ListView los servicios instalados.Cuando el usuario selecciona uno de los servicios de la lista, se hacen una serie de comprobaciones para saber el estado del servicio y qué tipo de acciones permite, todo esto se realiza en un procedimiento llamado comprobarEstado, el cual se usará también desde los eventos de los botones.
Veamos el código del evento SelectedIndexChanged del ListView y el procedimiento que comprueba el estado del servicio seleccionado de esa lista.
Private Sub serviciosLv_SelectedIndexChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles serviciosLv.SelectedIndexChanged Try itemActual = serviciosLv.SelectedItems(0) servicio = DirectCast(itemActual.Tag, ServiceController) comprobarEstado(servicio) Catch itemActual = Nothing comprobarEstado() End Try End SubEn este evento se asigna el elemento seleccionado a la variable itemActual, así como el servicio asociado a ese elemento del ListView a la variable servicio, (para hacer la asignación hacemos una conversión de tipos de forma que Option Strict On no nos lo impida, además de que es la forma recomendada, al menos desde mi punto de vista), y se llama al procedimiento comprobarEstado pasándole como parámetro el servicio a comprobar.
En el caso de que se produzca algún error... no es probable, pero si posible, así que nos curamos en salud y si ocurre una excepción, se asigna un valor nulo al elemento actual y se llama al procedimiento comprobarEstado sin parámetros, el cual simplemente deshabilitará los botones.El siguiente código es el del procedimiento sobrecargado comprobarEstado, el cual se usa para actualizar los botones y mostrar en el elemento de la lista el estado que corresponda, ya que, como te he comentado, este procedimiento se llamará desde los eventos de los botones.
Veamos el código y a continuación lo explico un poco.Private Sub comprobarEstado() iniciarBtn.Enabled = False iniciarParametrosBtn.Enabled = False pausarBtn.Enabled = False detenerBtn.Enabled = False continuarBtn.Enabled = False reiniciarBtn.Enabled = False End Sub Private Sub comprobarEstado(ByVal elServicio As ServiceController) Dim servStatus As ServiceControllerStatus ' iniciarBtn.Enabled = False iniciarParametrosBtn.Enabled = False pausarBtn.Enabled = False detenerBtn.Enabled = False continuarBtn.Enabled = False reiniciarBtn.Enabled = False ' Try servicio = elServicio servicio.Refresh() servStatus = servicio.Status ' If servicio.CanPauseAndContinue Then pausarBtn.Enabled = (servStatus = ServiceControllerStatus.Running) continuarBtn.Enabled = (servStatus = ServiceControllerStatus.Paused) End If If servicio.CanStop Then detenerBtn.Enabled = (servStatus = ServiceControllerStatus.Running) End If iniciarBtn.Enabled = (servStatus = ServiceControllerStatus.Stopped) iniciarParametrosBtn.Enabled = iniciarBtn.Enabled reiniciarBtn.Enabled = detenerBtn.Enabled Catch iniciarBtn.Enabled = False iniciarParametrosBtn.Enabled = False pausarBtn.Enabled = False detenerBtn.Enabled = False continuarBtn.Enabled = False reiniciarBtn.Enabled = False End Try ' If Not itemActual Is Nothing Then Try itemActual.SubItems(1).Text = servicio.Status.ToString Catch ex As Exception MessageBox.Show(ex.Message, "Error al comprobar el estado del servicio") End Try End If End SubDe estos dos procedimientos, el primero, el que no recibe ningún parámetro, simplemente deshabilita los botones. En el otro, el que recibe un parámetro del tipo ServiceController, se hacen una serie de comprobaciones para poder habilitar/deshabilitar adecuadamente los botones; en principio se deshabilitan todos y se asigna a la variable servicio el servicio que se ha pasado como parámetro.
Llamamos al método Refresh, ya que nos interesa "actualizar" el estado para que la información que se comprueba a continuación tenga los valores correctos, esto es necesario ya que este procedimiento se llama desde los eventos de los botones y debe reflejar el estado "más reciente" del servicio: el que se haya podido asignar en esos eventos.
A continuación asignamos el estado actual a una variable del tipo de la enumeración que la propiedad Status devuelve, cuyo valor usaremos en las siguientes líneas.
Lo primero que comprobamos es si el servicio se puede pausar y posteriormente continuar, si es así, el valor devuelto por la propiedad CanPauseAndContinue devolverá un valor verdadero. En el caso de que se permitan esas acciones, se comprueba si el servicio está en ejecución y se habilitará el botón Pausar; si el servicio está en pausa, se habilitará el botón Continuar.
En estos dos casos se comprueba si el valor devuelto por la propiedad Status es el adecuado para cada una de esas dos acciones, es decir, si está en ejecución, el valor de Status será Running y en caso de que esté en pausa, ese valor será Paused.
La siguiente comprobación que hacemos es saber si el servicio se puede detener, para ello comprobamos el valor de la propiedad CanStop, en caso de que sea verdadero, habilitaremos el botón Detener si el servicio está en ejecución.Una vez que hemos comprobado las propiedades que nos informan sobre las posibles acciones que podemos realizar con el servicio seleccionado, comprobamos si hay que actualizar la información mostrada en el ListView, para ello comprobamos si la variable itemActual está asignada (no contiene un valor nulo), en cuyo caso asignamos a la segunda columna el estado actual del servicio.
Nota:
Cuando creemos el Servicio de Windows, tendremos ocasión de ver cómo poder darle esa misma funcionalidad, de forma que seamos nosotros los que decidamos si el servicio se puede pausar, etc. Pero eso será dentro de un momento, así que... paciencia por ahora.Para terminar con el código de la utilidad para administrar los servicios de Windows, vamos a ver los eventos producidos al pulsar en los distintos botones de la aplicación. El código del botón Refrescar simplemente llamará al procedimiento mostrarServicios indicando en el parámetro el texto que el usuario haya escrito en la caja de textos correspondiente. El resto de eventos utilizará algunos de los métodos con los que podemos iniciar, detener, pausar y continuar un servicio previamente pausado. Veamos el código para comprender mejor cómo usar esos métodos.
Private Sub refrescarBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles refrescarBtn.Click mostrarServicios(equipoTxt.Text) End Sub ' ' Private Sub iniciarBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles iniciarBtn.Click Try servicio.Start() comprobarEstado(servicio) Catch ex As Exception MessageBox.Show(ex.Message, "Error al iniciar el servicio") End Try End Sub ' Private Sub iniciarParametrosBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles iniciarParametrosBtn.Click Try If parametrosTxt.Text <> "" Then Dim args(0) As String ' args(0) = parametrosTxt.Text servicio.Start(args) Else servicio.Start() End If comprobarEstado(servicio) Catch ex As Exception MessageBox.Show(ex.Message, "Error al iniciar el servicio con parámetros") End Try End Sub ' Private Sub detenerBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles detenerBtn.Click Try If servicio.CanStop Then servicio.Stop() comprobarEstado(servicio) End If Catch ex As Exception MessageBox.Show(ex.Message, "Error al detener el servicio") End Try End Sub ' Private Sub pausarBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles pausarBtn.Click Try If servicio.CanPauseAndContinue Then If servicio.Status = ServiceControllerStatus.Running Then servicio.Pause() comprobarEstado(servicio) End If End If Catch ex As Exception MessageBox.Show(ex.Message, "Error al pausar el servicio") End Try End Sub ' Private Sub continuarBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles continuarBtn.Click Try If servicio.CanPauseAndContinue Then If servicio.Status = ServiceControllerStatus.Paused Then servicio.Continue() comprobarEstado(servicio) End If End If Catch ex As Exception MessageBox.Show(ex.Message, "Error al continuar el servicio") End Try End Sub ' Private Sub reiniciarBtn_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles reiniciarBtn.Click If servicio.Status = ServiceControllerStatus.Running Then Try servicio.Stop() servicio.Refresh() Catch ex As Exception MessageBox.Show(ex.Message, "Error al reiniciar el servicio") End Try End If If servicio.Status = ServiceControllerStatus.Stopped Then Try servicio.Start() Catch ex As Exception MessageBox.Show(ex.Message, "Error al reiniciar el servicio") End Try End If comprobarEstado(servicio) End SubComo podemos comprobar después de cada llamada a los métodos, llamamos al procedimiento comprobarEstado para que se actualice la información en el ListView así como en los botones, para que reflejen si pueden ser usados o no después del cambio de estado realizado en el servicio.
Como se ha comentado, la variable servicio tendrá la referencia al servicio que se ha seleccionado de la lista, por tanto en estos procedimientos de eventos la utilizamos para llamar a los métodos correspondientes, que en el caso del botón Iniciar será el método Start. Por otro lado, el botón Iniciar con parámetros se usará para iniciar el servicio pasándole los argumentos necesarios, esos argumentos se indicarán en un array, en este ejemplo sólo usamos un argumento, por tanto creamos un array de un solo elemento y es ese array el que utilizamos en la llamada al método Start. Si no se indicara nada en la caja de textos de los parámetros, hacemos una comprobación y de ser así, simplemente se llama al método Start sin parámetros. Una vez llamado al método que nos permite iniciar el servicio, llamamos al procedimiento comprobarEstado.
Para detener el servicio, primero comprobamos si se puede parar, para ello comprobamos el valor de CanStop, si es verdadero, lo paramos mediante el método Stop y a continuación actualizamos la información.
En el código que hace una pausa en el servicio, comprobamos si eso se puede hacer, para ello usamos la propiedad CanPauseAndContinue, ya que como he comentado anteriormente, si esa propiedad devuelve un valor verdadero es que podemos detener momentáneamente el servicio para después volver a ponerlo en marcha. Como es natural, sólo podremos pausar el servicio si está en ejecución, por tanto, antes de utilizar el método Pause, comprobamos si el servicio está ejecutándose, para lo cual comprobamos si el contenido de la propiedad Status es ServiceControllerStatus.Running.
En el caso de querer continuar la ejecución de un servicio que se encuentra en modo de pausa, hacemos las comprobaciones pertinentes antes de usar el método Continue. No lo explico más a fondo ya que la explicación dada anteriormente es aplicable en este caso, aunque el estado que se comprueba es ServiceControllerStatus.Paused.
Por último, para reiniciar un servicio, como en esta ocasión no existe un método específico para esa acción, primero lo detenemos y después lo iniciamos, que es lo que me imagino que habría que hacer, aunque en la documentación de Visual Studio .NET no he visto nada que así lo indique, pero...
Debido a que en esta acción debemos comprobar si el servicio está detenido para volver a activarlo, he utilizado el método Refresh para que al llegar a la siguiente comparación esté "actualizado" el estado del servicio. En caso de que el servicio no permita que se detenga, no ocurrirá nada y nuestra acción simplemente no tendrá ningún efecto; aunque de ser así, el botón Reiniciar no estaría habilitado, ya que en el procedimiento comprobarEstado sólo se habilita ese botón si el servicio permite que se pueda detener.Para finalizar con esta primera parte del artículo, sólo me queda aclarar que los controles del formulario están "anclados" de forma que se ajusten al tamaño del mismo, para que nos resulte más cómodo ver el contenido de la información mostrada en las columnas del ListView, esto sólo lo digo por si quieres crear el formulario por tu cuenta sin necesidad de usar el código fuente incluido en el fichero ZIP.
Crear un Servicio para Windows
Bien, una vez visto el código de la utilidad para administrar los servicios de Windows, vamos ahora a crear un servicio.
Como te comenté al principio, el servicio que vamos a crear simplemente se encargará de comprobar si cierta aplicación está o no en ejecución, si no es así, la ejecutará. Este servicio también permitirá cerrar una aplicación que ya esté ejecutándose. Para que nuestro servicio sepa qué aplicación (o proceso) debe "controlar", se lo indicaremos por medio de un parámetro, el cual podemos especificarlo mediante la utilidad que acabamos de ver.
En un mundo real, o sea, en el caso de que realmente quisiéramos tener un Servicio de Windows con estas características, deberíamos darle la posibilidad de que cuando el servicio se inicie pueda hacer esa comprobación, y debido a la forma en que los servicios se inician, normalmente al iniciarse el sistema, tendríamos que utilizar un fichero de configuración, (por ejemplo el típico fichero INI), para que podamos indicar la aplicación a monitorear y la acción a realizar sobre ella: iniciarla o detenerla. Pero en este ejemplo, el servicio lo vamos a configurar para que se inicie manualmente, de forma que podamos iniciarlo cuando queramos y así poder indicarle, mediante parámetros, la aplicación a monitorear.Nota:
He de aclarar que cuando un servicio de Windows inicia una aplicación, ésta suele iniciarse en modo oculto, por tanto, si nuestro Servicio de Windows se inicia de forma automática con el sistema, deberíamos tener esto presente, para no llevarnos una sorpresa si no vemos dicha aplicación.Para la creación del Servicio de Windows vamos a crear un nuevo proyecto, el cual podemos añadirlo a la solución de la utilidad para administrar los servicios de Windows. El tipo de proyecto será del tipo de Servicios Windows y le daremos el nombre servicioWindows; en la figura 2 vemos el cuadro de diálogo de nuevo proyecto.
Figura 2, nuevo proyecto del tipo Servicio de WindowsAl crear un proyecto de este tipo tendremos un fichero llamado Service1.vb y el habitual AssemblyInfo.vb, además, en las referencias, tendremos la librería System.ServiceProcess.dll la cual es necesaria para manejar este tipo de proyecto.
Si echamos un vistazo al código añadido por Visual Studio .NET comprobaremos que la clase creada está derivada de System.ServiceProcess.ServiceBase y se incluyen, entre otras cosas, dos procedimientos: OnStart y OnStop, los cuales serán llamados por el sistema para iniciar y detener el servicio respectivamente.
Si en lugar de llamarse Service1, que es el nombre que el IDE de Visual Studio le da a esta clase, queremos que tenga otro nombre, por ejemplo PruebaSW, además de cambiar el nombre de la clase, tendremos que mostrar la "región" oculta, buscar el procedimiento Sub Main, en el cual se crea una nueva instancia de este servicio, y cambiar ahí el nombre por el que nosotros queramos que tenga.
Veamos el código completo de esta clase una vez hecho el cambio del nombre Service1 por PruebaSW:Imports System.ServiceProcess Public Class PruebaSW Inherits System.ServiceProcess.ServiceBase #Region " Código generado por el Diseñador de componentes " Public Sub New() MyBase.New() ' El Diseñador de componentes requiere esta llamada. InitializeComponent() ' Agregar cualquier inicialización después de la llamada a InitializeComponent() End Sub 'UserService reemplaza a Dispose para limpiar la lista de componentes. Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub ' El punto de entrada principal para el proceso <MTAThread()> _ Shared Sub Main() Dim ServicesToRun() As System.ServiceProcess.ServiceBase ' Se puede ejecutar en el mismo proceso más de un servicio NT. Para agregar ' otro servicio a este proceso, cambie la siguiente línea a fin de ' crear otro objeto de servicio. Por ejemplo, ' ' ServicesToRun = New System.ServiceProcess.ServiceBase () {New Service1, New MySecondUserService} ' ServicesToRun = New System.ServiceProcess.ServiceBase() {New PruebaSW()} System.ServiceProcess.ServiceBase.Run(ServicesToRun) End Sub 'Requerido por el Diseñador de componentes Private components As System.ComponentModel.IContainer 'NOTA: el Diseñador de componentes requiere el siguiente procedimiento 'Se puede modificar utilizando el Diseñador de componentes. No lo modifique ' con el editor de código. <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() components = New System.ComponentModel.Container() Me.ServiceName = "PruebaSW" End Sub #End Region Protected Overrides Sub OnStart(ByVal args() As String) ' Agregar código aquí para iniciar el servicio. Este método debería poner en movimiento ' los elementos para que el servicio pueda funcionar. End Sub Protected Overrides Sub OnStop() ' Agregar código aquí para realizar cualquier anulación necesaria para detener el servicio. End Sub End ClassEn negrita he resaltado los tres sitios en los que hay que cambiar el nombre de la clase, además de que deberíamos cambiar también el nombre del fichero, aunque eso no afecta para nada al nombre de este Servicio de Windows.
Nota:
Tal como puedes comprobar, en el procedimiento Main se crea un array (ServicesToRun) con los servicios que queramos ejecutar, esto es así por si en este mismo proyecto queremos añadir más de un servicio y queremos que se ejecuten todos. En caso de que esa sea tu intención, todo lo que veamos a partir de ahora será aplicable a cada uno de esos servicios del proyecto.Seguramente te preguntarás porqué tenemos código "parecido" al usado en los formularios, en particular el procedimiento InitializeComponent, ya que esto nos puede inducir a pensar que podemos añadir controles al componente, y es cierto, podemos añadir controles a un Servicio de Windows, pero realmente no se mostrarán cuando esté en ejecución, ya que un Servicio de Windows no presenta ninguna interfaz gráfica al usuario, al menos eso es lo que la documentación indica y lo que podemos deducir después de crear este tipo de ejecutable.
Sigamos con la configuración de nuestro servicio de prueba.
Ahora vamos a configurar el proyecto, lo primero que debemos hacer es cambiar el objeto inicial del ejecutable, ya sabes que esto es algo que debemos hacer cuando cambiamos el nombre de la clase que el IDE de Visual Studio .NET utiliza al crear cualquier clase de proyectos del tipo exe.
Y ya que estamos con las propiedades, vamos a asignar el valor On a Option Strict.Tal y como podemos ver, un Servicio de Windows es en realidad un ejecutable, pero los servicios no son aplicaciones o ejecutables "normales", es decir no se puede iniciar desde la línea de comandos o haciendo doble-click en el ejecutable. Para que un Servicio de Windows se inicie hay que instalarlo para que, si así lo indicamos, se inicie automáticamente la próxima vez que el sistema se inicie; aunque también lo podemos iniciar con una utilidad como la que hemos creado al principio de este artículo o usando Servicios del grupo de Herramientas administrativas.
Los servicios de Windows deben incluir una clase de tipo instalador por cada uno de los servicios que el proyecto incluya.
Por tanto, vamos a añadir un nuevo elemento al proyecto, será una clase del tipo Clase del instalador, a la que le daremos el nombre InstallerPruebaSW, tal como podemos ver en la figura 3.
Figura 3, una clase instalador para el Servicio de WindowsEn esta clase podemos indicar algunas de las características que tendrá el Servicio de Windows, como por ejemplo la forma en que se iniciará, en que cuenta del sistema se ejecutará el servicio, etc.
El código que incluye de forma automática el IDE de Visual Studio .NET no es necesario, al menos en su totalidad, por tanto podemos quitar el procedimiento InitializeComponents así como el método Dispose, que simplemente se encarga de llamar al procedimiento del mismo nombre del objeto components. Por tanto sólo nos quedaríamos con el constructor de la clase que será el sitio en el que indicaremos la forma en la que se iniciará el servicio.
Si buscamos en la ayuda de Visual Studio .NET, nos encontraremos con un ejemplo de cómo usar esta clase junto con un Servicio de Windows, de ese ejemplo es de donde he "tomado" la forma de configurar esta clase, así que si notas un parecido, que no te sorprenda... je, je... de todas formas, para eso está la ayuda, para que nos ayude en lo que no sepamos y como yo de esto no sabía nada, pues...
Te dejo también el link de la ayuda, para que puedas además informarte mejor sobre este tema, esta misma dirección está en la lista de temas relacionados del final de este artículo, ya sabes que lo que está en negrita puede que tengas que cambiarlo para que te funcione en tu ayuda de VS.NET:
ms-help://MS.MSDNVS.3082/cpref/html/frlrfSystemServiceProcessServiceInstallerClassTopic.htm
Y aquí te muestro el código de esta clase instaladora para usarla con nuestro Servicio de Windows:
Imports System.ServiceProcess Imports System.ComponentModel Imports System.Configuration.Install <RunInstaller(True)> Public Class InstallerPruebaSW Inherits System.Configuration.Install.Installer ' Private serviceInstaller1 As ServiceInstaller Private processInstaller As ServiceProcessInstaller Public Sub New() ' Instantiate installers for process and services. processInstaller = New ServiceProcessInstaller() serviceInstaller1 = New ServiceInstaller() ' The services will run under the system account. processInstaller.Account = ServiceAccount.LocalSystem ' The services will be started manually. serviceInstaller1.StartType = ServiceStartMode.Manual ' ServiceName must equal those on ServiceBase derived classes. serviceInstaller1.ServiceName = "PruebaSW" ' Add installers to collection. Order is not important. Installers.Add(serviceInstaller1) Installers.Add(processInstaller) End Sub ' End ClassEn esta clase simplemente asignamos la cuenta en la que se ejecutará el servicio, (por medio de la propiedad Account), en este caso es en la cuenta LocalSystem, así como la forma en que se iniciará, (mediante la propiedad StartType), si indicamos el valor Automatic, se iniciará automáticamente con el sistema, incluso si ningún usuario inicia una sesión, aunque para este ejemplo, indicaremos el valor Manual para que se inicie de forma manual, en breve verás el porqué. En la propiedad ServiceName de la clase ServiceInstaller tenemos que indicar el nombre del servicio que queremos instalar. En caso de que nuestro proyecto tenga más de un servicio, tendremos que crear un objeto de este tipo para cada uno de los servicios contenidos en el proyecto.
Para ir finalizando temas, te comentaré que el ejecutable con el o los servicios se instalará en el sistema usando la utilidad InstallUtil.exe del .NET Framework, esa utilidad se usará tanto para instalar como desinstalar un Servicio de Windows.
La forma de usar esa utilidad con la aplicación que vamos a crear sería la siguiente:
InstallUtil.exe servicioWindows.exe y para desinstalarlo usaremos: InstallUtil.exe /u servicioWindows.exe
Ahora nos centraremos en el código del servicio que estamos creando.
Debido a que este servicio tendrá que comprobar si una aplicación en particular está o no en ejecución de forma que siempre se asegure de que está en funcionamiento, salvo que se le indique lo contrario, usaremos un temporizador para hacer esa comprobación.Otra cosa que debemos decidir es si queremos que el servicio se pueda pausar, y posteriormente reanudar, si es así tendremos que hacer dos cosas:
La primera es asignar un valor verdadero a la propiedad CanPauseAndContinue, y la segunda será definir dos procedimientos los cuales serán llamados al utilizar los métodos Pause y Continue, el nombre de estos procedimientos será OnPause y OnContinue respectivamente.
En nuestro servicio de prueba, vamos a dar la posibilidad de realizar esas acciones, por tanto vamos a asignar la siguiente declaración en el constructor de la clase PruebaSW:Public Sub New() MyBase.New() ' El Diseñador de componentes requiere esta llamada. InitializeComponent() ' Agregar cualquier inicialización después de la llamada a InitializeComponent() 'CanStop = True ' este valor es el que tiene por defecto CanPauseAndContinue = True End SubComo puedes comprobar por el comentario, el valor predeterminado de la propiedad CanStop es True, por tanto, salvo que se indique lo contrario, los servicios creados con Visual Studio .NET siempre se podrán detener. Para que esto sea así, el servicio deberá implementar un procedimiento llamado OnStop, el cual ya está definido en la plantilla de la clase creada por el IDE, lo mismo que lo está el procedimiento que será llamado al iniciar el servicio: OnStart. Este último método se llamará cuando se inicie el servicio al usar el método Start o cuando se inicia el servicio de forma automática.
Nota:
Es importante saber que el método OnStart es llamado (o invocado) cuando se inicia el servicio, lo cual ocurre justo después de que se instancie el objeto en la memoria, el constructor sólo se llamará al crear el servicio, el cual permanecerá en memoria hasta que se cierre el sistema, por tanto, sólo será creado una sola vez. Esto quiere decir que si se detiene y se vuelve a iniciar el servicio no se destruirá y se volverá a crear, simplemente se llamará a los métodos correspondientes. En la documentación de Visual Studio .NET este punto es claro, pero creo que merece la pena recordarlo para que no se cometa el error de pensar que cuando un servicio se detiene y posteriormente se vuelve a iniciar el objeto asociado se vuelve a instanciar. Por tanto no se deberían asignar recursos en el constructor que posteriormente se vayan a liberar en el método OnStop, cualquier recurso que se vaya a liberar en ese método se debe crear en el procedimiento OnStart.
Como podrás comprobar en un momento, la implementación de estos métodos, (los cuales se declaran como protegidos), va a ser bastante simple, por la sencilla razón de que todo el trabajo que nuestro servicio hará estará en el evento del temporizador, por tanto estos procedimientos simplemente manipularán el estado del mencionado temporizador.
Veamos el código de estos cuatro procedimientos, así como la declaración del temporizador y el intervalo predeterminado:Private WithEvents temporizador As Timers.Timer Private intervalo As Integer = 50000 Protected Overrides Sub OnStart(ByVal args() As String) ' Agregar código aquí para iniciar el servicio. Este método debería poner en movimiento ' los elementos para que el servicio pueda funcionar. ' ' empezar con un intervalo pequeño para que se inicie el evento del temporizador ' nada más empezar, después se asignará el valor configurado temporizador = New Timers.Timer(100) temporizador.Start() End Sub Protected Overrides Sub OnStop() ' Agregar código aquí para realizar cualquier anulación necesaria para detener el servicio. temporizador.Stop() End Sub Protected Overrides Sub OnPause() ' si se detiene temporalmente el servicio temporizador.Stop() End Sub Protected Overrides Sub OnContinue() ' si s continúa el servicio, volver a iniciar el temporizador temporizador.Start() End SubTal como podemos comprobar, hemos definido una variable del tipo Timers.Timer, así como una variable que tendrá el valor del tiempo de intervalo entre cada evento del temporizador, el valor del temporizador será en milisegundos, por tanto el evento del temporizador se diparará cada 5 segundos.
En los procedimientos OnStop y OnPause se detiene el timer y en los otros dos procedimientos, OnStart y OnContinue, se vuelve a activar dicho temporizador. En el caso de OnStart, creamos una nueva instancia de la clase Timer a la que asignamos un intervalo pequeño, con la intención de que el primer intervalo se produzca de forma más o menos inmediata, así nos aseguramos de que las comprobaciones que el servicio deba hacer, se hagan casi al iniciarse.
Como veremos dentro de un momento, en el evento Elapsed del temporizador asignaremos de forma correcta el valor que el temporizador deba tener.Pero antes de ver ese código recapitulemos un poco sobre lo que nuestro servicio hará, ya que, al fin y al cabo, será en el evento del temporizador donde se hará todo el trabajo.
Para simplificar, nuestro Servicio de Windows va a controlar si una aplicación está o no en ejecución, en caso de que no lo esté, se encargará de ejecutarla.
También se le podrá indicar si queremos que haga o no esta comprobación, de forma que podamos dejar en una especie de estado de espera o standby el servicio.
Toda estos cambios de estado, se le indicarán mediante parámetros al iniciar el servicio.Por tanto, necesitaremos dos variables: una para contener el nombre (y localización) del programa que queremos "controlar" y otra para que el servicio sepa si debe comprobar si está o no en ejecución.
Sabiendo esto, vamos a añadir estas líneas después de las que declaraba el temporizador y el intervalo:
' la aplicación que vamos a comprobar Private aplicación As String ' si se debe hacer la comprobación Private comprobar As BooleanCuando se inicie el servicio, se podrán pasar los parámetros para indicar la aplicación y/o si se debe seguir comprobando o no. Por tanto, tenemos que modificar el procedimiento OnStart para que se pueda comprobar si se han indicado o no dichos parámetros.
El formato de dichos parámetros será el siguiente:
Para indicar la aplicación a comprobar: nombreAplicación [/C]
Para indicar que no se compruebe si está en ejecución: /NC
Realmente sólo se comprueba si el valor del segundo parámetro es /C, ya que cualquier otra cosa indicada tras la barra (/) se tomará como si fuese /NC.Veamos el código completo del procedimiento OnStart:
Protected Overrides Sub OnStart(ByVal args() As String) ' Agregar código aquí para iniciar el servicio. Este método debería poner en movimiento ' los elementos para que el servicio pueda funcionar. ' ' en args() estarán los argumentos pasados al servicio ' Se indicará en la forma: ' aplicación [/C] para iniciar la comprobación de la aplicación indicada ' [aplicación] /NC para detener la comprobación ' Try Dim s As String = args(0).Trim If s <> "" Then Dim i As Integer ' ' primero averiguamos si se debe o no comprobar If s.ToUpper.IndexOf("/C") > -1 Then comprobar = True Else comprobar = False End If ' según se haya o no indicado el segundo parámetro i = s.IndexOf("/") Select Case i Case -1 ' no se indica el segundo parámetro ' debe ser el nombre de la aplicación aplicación = s comprobar = True Case Is > 0 ' se indica la aplicación, ' debe estar antes de la barra aplicación = s.Substring(0, i - 1).Trim End Select End If Catch ' si se produce un error, no asignar nada, ' se usarán los valores anteriores, si hay... End Try ' ' ' empezar con un intervalo pequeño para que se inicie el evento del temporizador ' nada más empezar, después se asignará el valor configurado temporizador = New Timers.Timer(100) temporizador.Start() End SubEste procedimiento se ejecuta cada vez que iniciamos el servicio, si usamos la aplicación para administrar servicios, podemos usar el botón Iniciar con parámetros para indicar el parámetro que nuestro Servicio de Windows necesita.
Por último vamos a ver el código del temporizador, que será la parte de nuestro servicio que se encargue de comprobar si la aplicación indicada está o no en ejecución y en caso de que no lo esté, la ponga en marcha, siempre que se haya indicado que se compruebe...
Private Sub temporizador_Elapsed(ByVal sender As Object, _ ByVal e As System.Timers.ElapsedEventArgs) _ Handles temporizador.Elapsed ' ' deshabilitar temporalmente el timer temporizador.Enabled = False ' ' asignar el nuevo valor del temporizador If intervalo <> temporizador.Interval Then temporizador.Interval = intervalo End If ' ' si se debe comprobar si está en ejecución If comprobar Then Try Dim procesos() As Process Dim nombreProceso As String ' ' el nombre del proceso suele ser el nombre de la aplicación ' sin la extensión nombreProceso = Path.GetFileNameWithoutExtension(aplicación) procesos = Process.GetProcessesByName(nombreProceso) If (procesos.Length = 0 OrElse (procesos Is Nothing)) Then ' si no está en ejecución, iniciarlo ' ' IMPORTANTE: ' Los ejecutables iniciados con un Servicio de Windows ' no se muestran: se ejecutan ocultos. ' Dim proceso As New Process() ' proceso.StartInfo.FileName = aplicación proceso.Start() End If Catch ' End Try End If ' ' volver a habilitar el timer temporizador.Enabled = True End SubUn punto importante, (el cual está indicado en los comentarios de este evento y al principio de este artículo), es que cualquier aplicación iniciada por un Servicio de Windows se ejecutará de forma oculta, sin mostrar la interfaz gráfica que tuviera, por tanto, sólo deberían "comprobarse" aplicaciones que no interactúen con el usuario.
Pero salvo este "pequeño" inconveniente, creo que ahora tendremos más claro cómo crear Servicios de Windows con Visual Basic .NET, que es al fin y al cabo lo pretendido con este artículo.
Temas relacionados
Para más información, puedes consultar la ayuda de Visual Studio .NET, ya que no todo lo que se puede hacer con los Servicios de Windows está tratado en este artículo y de esa forma podrás seguir investigando por tu cuenta.
En los links aquí indicados, la parte en negrita hace referencia a la ayuda de Visual Studio .NET tal y como la tengo configurada en mi equipo, (con la versión en español de la ayuda), por tanto es posible que tengas que cambiar esa parte de acuerdo a como tengas tu ayuda instalada.
Esos links los puedes "copiar y pegar" en la "dirección URL" del propio Visual Studio .NET o bien en el navegador que tengas instalado en tu equipo.ms-help://MS.VSCC/MS.MSDNVS.3082/Vbcon/html/vbconIntroductionToNTServiceApplications.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/Vbcon/html/vbtskretrievinglistsofservices.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/Vbcon/html/vborimonitoringwindowsservices.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/cpref/html/frlrfsystemserviceprocess.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/cpref/html/frlrfsystemserviceprocessservicebaseclasstopic.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/cpref/html/frlrfsystemserviceprocessservicecontrollerclasstopic.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/cpref/html/frlrfsystemserviceprocessserviceprocessinstallerclasstopic.htm
ms-help://MS.VSCC/MS.MSDNVS.3082/cpref/html/frlrfsystemserviceprocessserviceinstallerclasstopic.htm
Nos vemos.
Guillermo
Nerja, 21 de Enero / 5 de Febrero de 2003Aquí tienes el código de la utilidad para administrar los Servicios de Windows, así como el código del servicio de prueba.
Como te comenté anteriormente, se incluye tanto el código para Visual Basic como para C#
El código completo: ServiciosWindows.zip (30.20 KB)