Cómo crear y operar un servicio de Windows — de la diferencia con el Programador de tareas a la creación de servicios con BackgroundService

· Actualizado el: · · Servicio de Windows, Windows, .NET, C#, BackgroundService, Generic Host, Procesos en segundo plano, Operación, Tabla de decisión, Consultoría técnica

«El Programador de tareas cada 5 minutos ya no da abasto», «queremos mantener residente un demonio de supervisión que espere de forma constante los datos de un dispositivo», «necesitamos que, en cuanto se coloque un archivo en una carpeta, se procese en cuestión de segundos». Cuando se siguen atendiendo consultas sobre ejecución periódica, tarde o temprano el requisito madura hasta llegar a este mismo punto.

En este blog hemos escrito de forma continuada sobre automatización, con artículos como Automatización de procesos con Power Automate y Diseño operativo seguro del Programador de tareas. En el capítulo 8 del artículo sobre el Programador de tareas se trazó la línea de que «cuando se necesita sondeo por minutos o supervisión constante, entramos en el terreno de los procesos residentes», pero ¿cómo se crea en la práctica un servicio de Windows y cómo se pone en operación? Si esto se deja ambiguo y se recurre a «convertirlo en servicio sin más», el resultado son «servicios que no se pueden detener», «servicios que se caen sin que nadie se entere» y «servicios que funcionan con LocalSystem haciendo cualquier cosa».

En este artículo repasamos, en el orden en que se decide en la práctica, la tabla de decisión sobre si un proceso en segundo plano debe convertirse en un servicio de Windows, el conocimiento mínimo del funcionamiento de los servicios (el SCM o Administrador de control de servicios, el tipo de inicio y la sesión 0), la implementación con el Worker Service de .NET 8, el registro con sc.exe y las opciones de recuperación, la elección de la cuenta de ejecución y, por último, la detención segura.

1. Conclusión inicial

  • El criterio de elección — Para un proceso periódico con un intervalo de varios minutos o más, el Programador de tareas es suficiente. Cuando el requisito incluye espera constante (sockets, canalizaciones con nombre, FileSystemWatcher), reacción en cuestión de segundos o recuperación automática ante caídas, corresponde un servicio de Windows.
  • No se puede mostrar interfaz de usuario — Desde Windows Vista, los servicios se ejecutan en la sesión 0 y no pueden interactuar directamente con el usuario (no pueden mostrar interfaz). Si se necesita interfaz, hay que separar el servicio de la aplicación de interfaz y diseñarlos para que se comuniquen mediante comunicación entre procesos (IPC).1
  • La vía de implementación — En .NET, la vía oficial consiste en la plantilla Worker Service junto con AddWindowsService del paquete Microsoft.Extensions.Hosting.WindowsServices (o UseWindowsService si se usa la familia IHostBuilder). Se puede ejecutar tal cual como aplicación de consola, por lo que el desarrollo y la depuración son muchísimo más sencillos que en el desarrollo de servicios tradicional.2
  • Excepciones y reinicio — Desde .NET 6, una excepción no controlada dentro de BackgroundService detiene el host de forma predeterminada (BackgroundServiceExceptionBehavior.StopHost). Sin embargo, al tratarse de una «detención normal», el reinicio mediante las opciones de recuperación del SCM (Administrador de control de servicios) no se activa. Los fallos que deban provocar un reinicio deben terminar el proceso con un código de salida distinto de 0.32
  • Cuenta de ejecución — No elija LocalSystem por inercia. Como LocalSystem es la cuenta predeterminada de sc.exe create, si no presta atención el servicio empezará a funcionar con el nivel de privilegio más alto.4 Si el proceso se resuelve por completo en local, LocalService o una cuenta virtual son las candidatas; si en un dominio se accede a recursos compartidos o a una base de datos, la opción principal es gMSA (Group Managed Service Account, cuenta de servicio administrada por grupo: una cuenta dedicada cuya contraseña administra automáticamente el dominio).5
  • Recuperación y detención — Las opciones de recuperación (sc.exe failure) y el proceso de detención (HostOptions.ShutdownTimeout, con un valor predeterminado de 30 segundos6) deben diseñarse en el momento del registro. Un «servicio que no responde a la detención» es lo más detestado en operación.

2. Cuándo conviene un servicio — tabla de decisión entre Programador de tareas, servicio y aplicación residente

Cuando aparece un requisito con aspecto de proceso residente, hay tres opciones: el Programador de tareas, un servicio de Windows y una «aplicación residente registrada en el inicio» (del tipo que permanece en el área de notificación). A continuación comparamos sus características.

Aspecto Programador de tareas Servicio de Windows Aplicación residente (registro en el inicio)
Momento de inicio Se inicia cada vez, activado por hora o evento Residente desde el arranque del SO (antes del inicio de sesión) Desde que el usuario inicia sesión
¿Requiere sesión iniciada? Se puede evitar (ejecución no interactiva) No la requiere Sí la requiere (desaparece al cerrar sesión)
Interfaz de usuario No se puede mostrar (en configuración no interactiva) No se puede mostrar (sesión 0) Sí se puede mostrar (área de notificación, cuadros de diálogo)
¿Constante o periódico? Periódico (adecuado de horas a diario) Constante Constante (pero dentro de la sesión del usuario)
Permisos Cuenta de ejecución por tarea Cuenta de servicio (capítulo 6) Permisos del usuario que inició sesión
Supervisión y recuperación Historial + notificación propia Opciones de recuperación del SCM (reinicio automático) Ninguna (hay que construirla)
Esfuerzo de distribución Registro con XML / PowerShell sc.exe / instalador Registro en la clave Run, etc.

Los criterios de decisión son los siguientes.

  • Si es un proceso periódico de unas pocas veces al día a intervalos de horas, y no mantiene estado entre ejecuciones, use el Programador de tareas. Convertirlo en servicio a propósito y gestionar el temporizador por su cuenta resulta excesivo; el diseño operativo descrito en el artículo sobre el Programador de tareas sale mucho más barato.
  • Si lo esencial es la espera constante —esperar solicitudes por TCP o canalizaciones, supervisar carpetas con FileSystemWatcher, procesar una cola de forma secuencial, reaccionar en cuestión de segundos— entonces es un servicio de Windows. Si empieza a sondear en fragmentos con «tareas cada 5 minutos», es una señal de que está reimplementando de forma degradada un proceso residente.
  • Si lo esencial es la interfaz de usuario (operaciones desde el área de notificación, ventanas emergentes para el usuario), es una aplicación residente. Sin embargo, no funciona sin sesión iniciada, así que no sirve para usos de tipo servidor. Si «se necesita procesamiento en segundo plano de forma constante, pero también interfaz de usuario», la solución, como se explica en el siguiente capítulo, es separarlo en dos procesos: el servicio y la aplicación de interfaz.

Hay otro criterio que suele pasarse por alto: la recuperación. Un trabajo del Programador de tareas que falla se queda tal cual hasta la siguiente programación, mientras que en un servicio el SCM se encarga del reinicio automático (capítulo 5). El requisito de «que si se cae por la noche, se recupere por sí solo antes de la mañana» es, por sí mismo, un motivo para convertirlo en servicio.

3. Lo mínimo indispensable sobre el funcionamiento de los servicios — el SCM, el tipo de inicio y la sesión 0

3.1 El SCM y el tipo de inicio

El responsable último de los servicios de Windows es el Administrador de control de servicios (SCM). Gestiona el registro de servicios, intermedia las solicitudes de inicio y detención, y ejecuta las acciones de recuperación cuando algo falla. El proceso del servicio debe estar construido para conversar con el SCM siguiendo el protocolo establecido, pero esa parte la asume la biblioteca de .NET (capítulo 4), de modo que lo que nosotros decidimos como diseño se reduce a cuatro puntos: «tipo de inicio», «cuenta de ejecución», «recuperación» y «detención».

El tipo de inicio (tipo de arranque) tiene cuatro opciones.4

Tipo de inicio Comportamiento Cuándo usarlo
Automático (auto) Se inicia al arrancar el SO Base de los servicios que funcionan de forma constante
Automático (inicio retrasado) (delayed-auto) Se inicia un poco después que los demás servicios automáticos La primera opción para servicios de negocio. Evita la congestión inmediatamente después del arranque y las dependencias aún no iniciadas
Manual (demand) Se inicia solo cuando se solicita Servicios auxiliares que otras aplicaciones inician
Deshabilitado (disabled) No se puede iniciar Medida de detención o bloqueo

En los servicios de negocio, conviene usar el inicio retrasado como valor predeterminado. Inmediatamente después del arranque del SO, ni la red, ni la base de datos, ni los demás servicios están todavía disponibles, así que iniciar lo antes posible con «automático» tiende a provocar fallos en la primera conexión. La combinación estable consiste en introducir un desfase con el inicio retrasado y, además, absorber los fallos de conexión mediante reintentos dentro de ExecuteAsync, como se explica más adelante.

Hay otro punto que conviene conocer: el tiempo de espera del inicio. El SCM no espera indefinidamente el informe de finalización del inicio del servicio. Si se supera el tiempo de espera predeterminado (ServicesPipeTimeout, 30 segundos), se registran los eventos 7000 / 7011 y se considera que el inicio ha fallado.7 En otras palabras, no debe realizar dentro del «procesamiento de inicio» una inicialización pesada, como reintentos de conexión a la base de datos o la construcción de una caché grande. La regla de oro es que el inicio se complete de inmediato y que el procesamiento pesado se realice en el bucle principal (ExecuteAsync).

Si se resume en un solo diagrama la conversación entre el SCM y el proceso del servicio, el flujo es el siguiente. Tanto el inicio como la detención consisten en un intercambio de «solicitud» e «informe de finalización», y la particularidad de los servicios es que ese intercambio tiene un límite de tiempo.

BackgroundServiceProceso del servicioSCMAdministrador / arranque del SOBackgroundServiceProceso del servicioSCMAdministrador / arranque del SOSi no informa que está en ejecución dentrodel ServicesPipeTimeout de 30 segundos, se registran los eventos 7000 / 7011Si se supera el ShutdownTimeout de 30 segundos por defecto,se fuerza la detención sin esperar a que termine la limpiezaSolicitud de inicio (sc.exe start o inicio automático)Inicia el procesoInforma que está iniciando START_PENDINGInforma que está en ejecución SERVICE_RUNNINGInicia ExecuteAsyncBucle de espera y procesamientoSolicitud de detención (sc.exe stop o apagado)Notifica la solicitud de detenciónCancela stoppingTokenSale del bucle y StopAsync finalizaInforma que se ha detenido SERVICE_STOPPED

Implementar este intercambio es, en esencia, el desarrollo tradicional de servicios, pero en .NET lo hace por nosotros el ciclo de vida de servicio que introduce AddWindowsService (capítulo 4). Lo único que escribimos es el contenido de ExecuteAsync y la respuesta a la solicitud de detención (capítulo 7).

3.2 El aislamiento de la sesión 0 — los servicios no pueden mostrar interfaz de usuario

Desde Windows Vista, los servicios se ejecutan en una sesión aislada llamada sesión 0 y no pueden interactuar directamente con el usuario.1 Aunque dentro del servicio se llame a MessageBox.Show o se muestre un formulario, no aparece nada en la pantalla del usuario con sesión iniciada. Peor aún, se produce el clásico bloqueo (hang) en el que todo el procesamiento se detiene esperando indefinidamente que alguien pulse un botón Aceptar que nadie puede pulsar. Al migrar código antiguo a un servicio, compruebe siempre que no queden cuadros de mensaje pensados para mostrar errores.

Por qué están separadas las sesiones, a qué sesión se entra al conectarse por RDP, si los objetos con nombre pueden cruzar sesiones… el funcionamiento de las sesiones en sí se trata en «Cómo entender el aislamiento de sesiones en Windows». En este artículo solo necesitamos el punto de que «los servicios no pueden mostrar interfaz», así que continúe la lectura dando eso por sentado.

Cuando se necesita interfaz de usuario, la solución correcta es una configuración en la que se separan el servicio (sesión 0) y la aplicación de interfaz (sesión de usuario) en procesos distintos que se comunican mediante comunicación entre procesos (IPC). Es el diseño que la propia Microsoft recomienda: se usa un IPC como las canalizaciones con nombre, y el lado de la interfaz devuelve los resultados al servicio.1 Qué mecanismo de IPC elegir se detalla en el artículo hermano publicado el mismo día, «Tabla de decisión de comunicación entre procesos». Si además se monta la aplicación de interfaz sobre Generic Host, se puede unificar en ambos procesos el diseño de la inyección de dependencias, el registro de logs y la configuración (véase «Uso de Generic Host y BackgroundService en una aplicación de escritorio»).

4. Cómo crearlo en .NET — Worker Service y AddWindowsService

4.1 La plantilla y Program.cs

El desarrollo de servicios en .NET empieza con la plantilla Worker Service. Al venir incluida en el SDK, dotnet new worker genera el andamiaje inicial, y basta con agregar el paquete Microsoft.Extensions.Hosting.WindowsServices y llamar a AddWindowsService para que quede listo para funcionar como servicio de Windows.2 El funcionamiento de Generic Host, que sirve de base, se explica en «Qué es .NET Generic Host».

using App.MonitorService;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Incorpora el ciclo de vida que conversa con el SCM cuando se inicia como servicio de Windows.
// Cuando se inicia como consola, sigue funcionando con el ConsoleLifetime habitual
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "KsMonitor";
});

builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();

IHost host = builder.Build();
host.Run();

En un proyecto de estilo tradicional que use Host.CreateDefaultBuilder, en su lugar se llama a la extensión de IHostBuilder UseWindowsService(). Su función es la misma.

Hay una trampa del mismo tipo que el problema de «Iniciar en (opcional)» del artículo sobre el Programador de tareas. El directorio de trabajo actual cuando se inicia como servicio es C:\Windows\System32. Si se lee appsettings.json u otros archivos suponiendo una ruta relativa, el resultado es que «funciona en consola, pero como servicio no puede leer la configuración». Resuelva los archivos de configuración en relación con el ejecutable, o bien indique explícitamente la raíz de contenido pasando --contentRoot en el binpath del registro, tal como recomienda el tutorial oficial.2

4.2 ExecuteAsync — el diseño de stoppingToken y las excepciones

El cuerpo del servicio se escribe en ExecuteAsync, heredado de BackgroundService. Lo que hay que decidir aquí se reduce a dos puntos: «la respuesta a la solicitud de detención» y «cómo tratar las excepciones».

namespace App.MonitorService;

public sealed class MonitorWorker(
    MeasurementQueue queue,
    ILogger<MonitorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // Extrae un elemento de la cola y lo procesa. Pasa siempre el token también en las esperas
                    var item = await queue.DequeueAsync(stoppingToken);
                    await ProcessAsync(item, stoppingToken);
                }
                catch (Exception ex) when (ex is IOException or TimeoutException)
                {
                    // Fallo que se puede continuar: se registra y se reintenta tras una espera
                    logger.LogError(ex, "Error al procesar. Se reintentará en 30 segundos.");
                    await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                }
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Solicitud de detención del SCM. Es el flujo normal, así que no se hace nada.
            // La cláusula when se limita a la causa stoppingToken porque, si no, una cancelación
            // lanzada por un tiempo de espera interno se confundiría con una "detención normal"
            // y el worker quedaría detenido en silencio mientras el host sigue vivo
        }
        catch (Exception ex)
        {
            // Fallo inesperado: con el StopHost predeterminado el host se detendría de forma "normal"
            // y las opciones de recuperación del SCM (reinicio automático) no se activarían.
            // Se finaliza con un código de salida distinto de 0 para informar al SCM del fallo
            logger.LogCritical(ex, "Se finaliza el servicio por un error irrecuperable.");
            Environment.Exit(1);
        }
    }

    private Task ProcessAsync(Measurement item, CancellationToken token)
        => throw new NotImplementedException();
}
  • Pase stoppingToken a todas las esperas. Task.Delay, operaciones de E/S, esperas en colas. Basta con que en un solo punto se ignore el token en una espera larga para que ese punto se convierta en el origen de un «servicio que no responde a la detención» (capítulo 7).
  • Distinga entre fallos que se pueden continuar y fallos irrecuperables. Los cortes de red o los tiempos de espera temporales se capturan dentro del bucle, se registran y se reintentan. Un catch (Exception) que simplemente ignora el error y hace continue solo crea un servicio que gira en vacío en silencio. El criterio para clasificar cada caso se explica en «Tabla de decisión: terminar o continuar ante una excepción inesperada».
  • Conozca el comportamiento predeterminado ante excepciones no controladas. Antes de .NET 6, una excepción que escapaba de ExecuteAsync desaparecía sin dejar rastro, y el servicio se convertía en un «zombi» que parecía estar en ejecución pero no hacía nada. Desde .NET 6 el valor predeterminado es BackgroundServiceExceptionBehavior.StopHost, con lo que la excepción se registra en el log y el host se detiene.3 Sin embargo, esta detención es una «detención normal», por lo que aunque haya configuradas opciones de recuperación, no se produce el reinicio. La práctica que recomienda el tutorial oficial es que los fallos que deban recuperarse mediante las opciones de recuperación finalicen el proceso de forma explícita con Environment.Exit(1), tal como en el código anterior.2

4.3 Se puede ejecutar tal cual como consola — la razón principal por la que el desarrollo se ha simplificado

AddWindowsService solo cambia al ciclo de vida propio de un servicio cuando se inicia como servicio de Windows. Es decir, el mismo ejecutable funciona como una aplicación de consola normal con F5 en Visual Studio o con dotnet run. Los puntos de interrupción funcionan con normalidad, y al pulsar Ctrl+C se puede verificar en local todo el flujo, desde la solicitud de detención hasta StopAsync.

Sin embargo, que funcione en consola no garantiza que funcione como servicio. La diferencia de cuenta de ejecución (capítulo 6), la sesión 0 y el directorio de trabajo actual son los tres únicos puntos que hay que verificar como servicio en la máquina real. La causa de que «funcione en consola pero no como servicio» casi siempre se reduce a estos tres puntos.

5. Registro y operación — sc.exe, opciones de recuperación y el registro de eventos

5.1 Registro con sc.exe create

El ejecutable publicado (con dotnet publish; la publicación en un único archivo resulta cómoda de manejar) se registra en el SCM con sc.exe create. Ejecútelo en una sesión de PowerShell con privilegios de administrador.

sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "Servicio KS de ingesta de datos de medición"
sc.exe description "KsMonitor" "Servicio residente que ingiere datos de medición y los registra en la base de datos (responsable: Departamento de Sistemas de Información)"

Primero, la trampa clásica: tras el signo igual de binpath= o start= es obligatorio dejar un espacio. Es una peculiaridad propia de sc.exe: hasta el signo igual es el nombre de la opción, y el espacio antes del valor es obligatorio en la sintaxis (si se omite, falla).4 Además, si se omite obj=, el valor predeterminado es LocalSystem.4 Si se registra sin pensarlo, el servicio empezará a funcionar con el nivel de privilegio más alto, así que resuelva la elección del capítulo 6 en el momento del registro.

La configuración de description es discretamente importante. Dejar constancia de la información necesaria para que alguien que abra services.msc dentro de unos años pueda decidir «qué es este servicio, si se puede detener, a quién preguntar» elimina el coste de investigación posterior. Para eliminarlo, primero deténgalo y luego use sc.exe delete "KsMonitor".2

Si hay varios equipos de destino, o si las actualizaciones (sustitución de archivos) se producen de forma periódica, migre cuanto antes a un instalador (MSI, ServiceInstall de WiX, etc.) que se encargue en conjunto del registro, la actualización y la eliminación, en lugar de hacerlo a mano con sc.exe. Un procedimiento de registro manual siempre acaba generando algún equipo en el que se saltó un paso.

5.2 Opciones de recuperación — deje «si se cae, reiniciar» en manos del SCM

Una de las grandes ventajas de convertir algo en servicio son las opciones de recuperación estándar del SCM. Permiten configurar de forma declarativa el comportamiento cuando el proceso finaliza de forma anómala.2

sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000

Este ejemplo significa «en el primer y segundo fallo, reiniciar a los 60 segundos; a partir del tercero, reiniciar a los 5 minutos; el contador de fallos se reinicia cada 24 horas». La misma configuración se puede hacer desde la interfaz gráfica (propiedades de services.msc → pestaña «Recuperación»). Antes de construir su propia «tarea de vigilancia» o script de supervisión, use primero esta función estándar.

Solo hay que tener en cuenta dos puntos. Primero, como se explicó en el capítulo anterior, no se activa si el proceso no termina con un código distinto de 0. Una detención normal mediante StopHost queda fuera del alcance de la recuperación. Segundo, si el servicio se cae siempre inmediatamente después de iniciarse (por un error de configuración o porque no llega a la base de datos), se produce un bucle de reinicios. Por eso conviene dejar un intervalo más largo a partir del tercer intento, y también dejar preparado de antemano un estado en el que quede constancia en el registro de eventos (siguiente sección) de «por qué se cayó».

5.3 El uso combinado del registro de eventos y el log de archivo

En Windows, Host.CreateApplicationBuilder agrega automáticamente el proveedor de registro EventLog. Y al registro de eventos solo llega, de forma predeterminada, lo de nivel Warning o superior.2 Es fácil confundirse pensando que «al convertirlo en servicio han desaparecido todos los logs de Information», pero no han desaparecido: los ha filtrado el filtro predeterminado del registro de eventos. Si también quiere registrar eventos operativos como el inicio y la detención, indique explícitamente el nivel del proveedor EventLog en appsettings.json.

{
  "Logging": {
    "EventLog": {
      "SourceName": "KsMonitor",
      "LogLevel": {
        "Default": "Warning",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Hay un punto de atención. El origen de eventos debe estar registrado de antemano para poder escribir en él, y ese registro requiere privilegios de administrador. Omitir SourceName no permite escapar del problema: como AddWindowsService usa de forma predeterminada el nombre de la aplicación como nombre del origen2, en cualquier caso se parte de un «origen no registrado». Una cuenta de privilegio mínimo como LocalService no puede crear el origen en tiempo de ejecución, y si la creación falla, la operación arranca sin que se escriba nada en el registro de eventos. Realice el registro desde el instalador (o desde un proceso de configuración con privilegios de administrador). Es la misma idea que «separar CreateEventSource hacia el lado de la configuración» que se explicó en el capítulo 6 del artículo sobre el Programador de tareas.

El criterio general es: en el registro de eventos, solo lo que el responsable de operación deba ver (inicio, detención, fallo, recuperación), y el trazado detallado del procesamiento en un log de archivo propio. Los requisitos mínimos para mantener un log de archivo propio (rotación, comportamiento ante un fallo de escritura) se resumen en «Requisitos mínimos de un logger propio», y el diseño para dejar evidencia cuando el proceso entero se cae se explica en «Diseño para conservar logs y volcados en caso de caída». En una configuración con reinicio automático mediante opciones de recuperación, que quede un registro del instante exacto de la caída es la única pista disponible.

5.4 Cómo verificar que el registro se realizó correctamente — qué mirar en la línea de comandos y en la interfaz gráfica

Una vez registrado, revise en orden estos tres puntos para confirmar que «funciona con la configuración prevista y con la cuenta prevista».

Verificación de la configuración (línea de comandos). sc.exe qc "KsMonitor" muestra el contenido del registro. Hay que fijarse en tres líneas: BINARY_PATH_NAME (si apunta al ejecutable publicado), START_TYPE (si quedó como automático, automático retrasado o manual) y SERVICE_START_NAME (si la cuenta de ejecución es la decidida en el capítulo 6). El estado de funcionamiento se ve en STATE de sc.exe query "KsMonitor", o con Get-Service KsMonitor en PowerShell. El valor actual de las opciones de recuperación se puede leer con sc.exe qfailure "KsMonitor".

Verificación de la configuración (interfaz gráfica). Presione la tecla de Windows + R y escriba «services.msc» para abrir «Servicios» (la misma pantalla se puede abrir también desde «Administración de equipos» → «Servicios y aplicaciones» → «Servicios»). Haga clic con el botón derecho sobre el servicio en cuestión → «Propiedades»: la pestaña «General» muestra el tipo de inicio y la descripción configurada en 5.1, la pestaña «Iniciar sesión» muestra la cuenta de ejecución, y la pestaña «Recuperación» muestra el comportamiento ante el primer fallo, el segundo y los siguientes.

Verificación del registro de eventos. Presione la tecla de Windows + R y escriba «eventvwr.msc» para abrir el Visor de eventos, y en «Registros de Windows» → «Sistema» consulte los eventos del origen «Service Control Manager». Ahí quedan registrados los eventos 7000 / 7011 cuando el inicio no llega a tiempo con el informe de finalización7, y los eventos 7031 (cuando hay acción de recuperación) y 7034 (cuando no la hay) cuando el servicio finaliza de forma anómala8. Por otro lado, los propios logs de la aplicación, cuyo nivel se configuró en 5.3, se filtran en «Registros de Windows» → «Aplicación» por el nombre del origen (SourceName, cuyo valor predeterminado es el nombre de la aplicación2), ya que el destino predeterminado del proveedor EventLog es «Application»9. Conviene recordar que la vida del servicio se mira en «Sistema», y el contenido de la aplicación en «Aplicación», para no tener que buscar a ciegas cuando ocurre un incidente.

6. Cuenta de ejecución — no elija LocalSystem por inercia

El servicio se ejecuta en el contexto de seguridad de la cuenta indicada, y el SCM inicia sesión con esa cuenta al arrancar y adjunta un token al proceso.10 Es la versión para servicios del tema de «quién ejecuta el proceso» que se trató en el capítulo 3 del artículo sobre el Programador de tareas. A continuación se enumeran las opciones.

Cuenta Permisos Identidad en red Gestión de contraseña Cuándo usarla
LocalSystem Extremadamente amplios (equivalentes al SO) Cuenta de equipo No requerida Evítela en principio. Tenga cuidado porque es el valor predeterminado de sc.exe create
LocalService Mínimos Anónima (no puede acceder a recursos compartidos) No requerida Primera candidata para procesos que se resuelven en local
NetworkService Mínimos Cuenta de equipo No requerida Procesos ligeros que acceden a recursos dentro del dominio
Cuenta virtual (NT SERVICE\nombre del servicio) Solo los concedidos Cuenta de equipo No requerida Permite asignar una ACL por servicio. Privilegio mínimo para recursos locales
gMSA Solo los concedidos El propio gMSA Administrada automáticamente por el DC La opción principal para servicios que acceden a recursos compartidos o bases de datos en el dominio

Hay tres puntos clave para decidir.

  • No elija LocalSystem solo porque “funciona”. Una vulnerabilidad en el servicio se traduce directamente en la toma de control de toda la máquina. Para saber si de verdad se necesitan privilegios equivalentes a los de administrador, el análisis de «Cuándo se necesitan privilegios de administrador» es directamente aplicable. En muchos casos, lo único que se necesita es «escribir en una carpeta concreta», y para eso basta con asignar la ACL de la carpeta a una cuenta virtual (NT SERVICE\KsMonitor). Las cuentas virtuales no requieren ni creación ni gestión de contraseña.5
  • La trampa de los recursos compartidos de red. LocalService aparece como anónimo en la red, por lo que el acceso a \\servidor\recurso falla. NetworkService, las cuentas virtuales y LocalSystem salen a la red como cuenta de equipo (DOMINIO\nombreDeMáquina$)5, así que en el lado del recurso compartido hay que conceder permisos a esa cuenta de equipo, tanto de recurso compartido como de NTFS. La causa habitual de «le di permiso al usuario, pero solo el servicio no puede leer» suele ser justo esta. Decida primero «con qué identidad sale a la red» y luego configure el lado del recurso compartido.
  • Si va a ejecutarlo con una cuenta de usuario normal, cuente con la gestión de la contraseña. Al usar una cuenta dedicada se necesita el derecho de “iniciar sesión como servicio”, y además, si la contraseña caduca, el inicio de sesión falla y el servicio no puede arrancar.10 Es la versión para servicios del problema de «la tarea muere en silencio al cambiar la contraseña». En un entorno de dominio, la solución correcta es eliminar este problema de raíz con gMSA, cuya contraseña administra automáticamente el dominio (la misma conclusión que en la sección 3.2 del artículo sobre el Programador de tareas).

7. Detención segura — ShutdownTimeout y el trabajo en curso

Repasemos el flujo de detención. Cuando el SCM emite una solicitud de detención —desde services.msc, sc.exe stop o el apagado del sistema operativo—, el ciclo de vida de servicio de .NET lo traduce en una detención del host (StopApplication), se cancela stoppingToken, ExecuteAsync termina y se llama a StopAsync de cada servicio. El tiempo que el host espera a que se complete esta secuencia de detención es HostOptions.ShutdownTimeout, y su valor predeterminado es de 30 segundos.611 Si no da tiempo, se fuerza la detención sin esperar a que termine la limpieza.

Si 30 segundos no bastan para terminar de escribir un elemento en curso, amplíe el tiempo calculándolo a partir del tiempo necesario.

builder.Services.Configure<HostOptions>(options =>
{
    // Calcúlelo a partir del tiempo máximo necesario para terminar el procesamiento en curso
    options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});

A partir de ahí, el diseño del proceso de detención se reduce a estos tres puntos.

  • Defina de antemano, como una secuencia ordenada, qué hacer tras la solicitud de detención. Dejar de aceptar trabajo nuevo → completar el procesamiento en curso (o interrumpirlo en un punto seguro y registrarlo de forma que se pueda reanudar) → limpiar conexiones y archivos temporales. Si no se define este orden, se cae en uno de dos extremos: abandonarlo todo en cuanto se detecta el token, o intentar terminarlo todo y agotar el tiempo.
  • No construya un «servicio que no responde a la detención». Basta con un solo bucle largo o una espera que ignore el token para que el servicio quede atascado en «deteniéndose» ante el SCM. Es el defecto más detestado en operación, ya que impide el reinicio de Windows Update y obliga a un kill manual en cada parada planificada del servidor. Como en el capítulo 4, compruebe el token en cada punto de corte del procesamiento y pase un CancellationToken a cualquier operación de E/S larga. Con la ejecución en consola se puede probar este mismo flujo de detención con Ctrl+C, así que se recomienda incluir en las pruebas previas al lanzamiento cuántos segundos tarda en terminar tras pulsar Ctrl+C.
  • Priorice un diseño que no se rompa aunque se le haga kill, por encima del esmero del proceso de detención. Un corte de energía o una finalización forzada pueden ocurrir por más elaborado que sea el proceso de detención. Transacciones, escritura atómica de archivos, unidades de trabajo que se puedan volver a ejecutar: lo esencial es un diseño en el que «aunque se muera a mitad de camino, la coherencia se recupere en el siguiente arranque»; un proceso de detención cuidadoso es, en el fondo, solo una cuestión de buenas maneras.

8. Resumen

La decisión es simple. Para procesos periódicos, el Programador de tareas; si se necesita espera constante, reacción en cuestión de segundos o recuperación automática, un servicio de Windows; si lo esencial es la interfaz de usuario, una aplicación residente. Una vez decidido que será un servicio, la vía estándar actual consiste en «desarrollarlo como consola y distribuirlo como servicio» mediante el Worker Service de .NET y AddWindowsService.

En el momento del registro hay cuatro puntos que decidir como diseño: el tipo de inicio (inicio retrasado para los servicios de negocio), la cuenta de ejecución (evitar LocalSystem y usar una cuenta virtual o gMSA), las opciones de recuperación (la combinación de sc.exe failure y Environment.Exit(1)) y la detención (ShutdownTimeout y la limpieza del trabajo en curso). Con solo dominar estos cuatro puntos y una forma de escribir ExecuteAsync que respete stoppingToken, un servicio de Windows deja de ser «algo especialmente difícil de construir». Por el contrario, un servicio que empieza a funcionar sin haber decidido estos cuatro puntos acaba volviendo, unos años después, con la triple carga de «no se puede detener», «se cae sin que nadie se entere» y «tiene demasiados privilegios para tocarlo». Reconstruir un servicio existente también es más rápido si se empieza haciendo un inventario de estos cuatro puntos.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC atiende consultas sobre la revisión de diseño de procesos en segundo plano (incluida la decisión de migrar desde el Programador de tareas) y sobre la investigación y reconstrucción de servicios de Windows «que no responden a la detención» o «que se caen sin que nadie se entere».

Referencias

  1. Microsoft Learn, Interactive Services. Trata que, desde Windows Vista, los servicios no pueden interactuar directamente con el usuario, que se ejecutan en la sesión 0, y que cuando se necesita interfaz de usuario se recomienda un diseño con una aplicación GUI en un proceso distinto que colabore mediante IPC.  2 3

  2. Microsoft Learn, Create Windows Service using BackgroundService. Tutorial oficial para convertir un Worker Service en un servicio de Windows. Trata AddWindowsService, que el proveedor EventLog tiene Warning como nivel predeterminado, el registro, la recuperación y la eliminación mediante sc.exe, y que las opciones de recuperación requieren un código de salida distinto de 0.  2 3 4 5 6 7 8 9 10

  3. Microsoft Learn, .NET 6 breaking change: Exception handling in hosting. Trata que, desde .NET 6, una excepción no controlada de BackgroundService.ExecuteAsync se registra en el log y, de forma predeterminada, detiene el host (BackgroundServiceExceptionBehavior.StopHost).  2

  4. Microsoft Learn, sc.exe create. Trata las especificaciones de start= (auto, demand, delayed-auto, etc.) y de obj= (con LocalSystem como valor predeterminado), y que es obligatorio dejar un espacio tras el signo igual de las opciones (si se omite, falla).  2 3 4

  5. Microsoft Learn, Service Accounts in Windows Server. Trata que las cuentas virtuales (NT SERVICE\nombre del servicio) no requieren gestión de contraseña y acceden a la red con las credenciales de la cuenta de equipo, y que la contraseña de gMSA la administra automáticamente el dominio.  2 3

  6. Microsoft Learn, .NET Generic Host. Trata el flujo del proceso de apagado del host y que, al detenerse, se espera durante un tiempo de espera de apagado configurable (30 segundos de forma predeterminada).  2

  7. Microsoft Learn, A service does not start, and events 7000 and 7011 are logged. Trata que el SCM espera el inicio del servicio durante el tiempo indicado por ServicesPipeTimeout y que, al superarlo, registra los eventos 7000 / 7011, así como el procedimiento para ampliar el tiempo de espera predeterminado.  2

  8. Microsoft Learn (archivo), Event ID 7031 — Service Stop Operations y Event ID 7034 — Service Stop Operations. Trata que el SCM registra la finalización inesperada de un servicio y el hecho de no haber podido reiniciarlo tras la acción de recuperación; que el evento 7031 (nombre de símbolo EVENT_SERVICE_CRASH) tiene el mensaje «El servicio finalizó de forma inesperada. Esto ha ocurrido N veces. Se ejecutará la siguiente acción de corrección dentro de M milisegundos», mientras que el 7034 (nombre de símbolo EVENT_SERVICE_CRASH_NO_ACTION) no incluye descripción de acción de corrección; que en ambos casos el origen es Service Control Manager; y que la acción de recuperación se puede modificar en la pestaña «Recuperación» de las propiedades del complemento «Servicios». 

  9. Microsoft Learn, EventLogSettings.LogName Property. Trata que el nombre del registro de eventos de destino del proveedor EventLog es, si no se especifica, “Application” de forma predeterminada. 

  10. Microsoft Learn, Service User Accounts. Trata que el servicio se ejecuta en el contexto de seguridad de la cuenta indicada, que el SCM inicia sesión al arrancar y que si la contraseña ha caducado el servicio no puede iniciarse, así como las cuentas especiales LocalService, NetworkService y LocalSystem.  2

  11. Microsoft Learn, HostOptions.ShutdownTimeout Property. Trata la definición de la propiedad que controla el tiempo de espera predeterminado de StopAsync

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cómo se decide entre el Programador de tareas y un servicio de Windows?
Si se trata de un proceso periódico con un intervalo de varios minutos o más y que no mantiene estado entre ejecuciones, el Programador de tareas es suficiente. Cuando el requisito incluye espera constante por TCP o canalizaciones (pipes), supervisión de carpetas con FileSystemWatcher, reacción en cuestión de segundos o recuperación automática ante caídas, entonces corresponde un servicio de Windows. Si empieza a sondear (polling) en fragmentos mediante «tareas cada 5 minutos», es una señal de que está reimplementando de forma degradada un proceso residente. Si lo esencial es la interfaz de usuario (por ejemplo, operaciones desde el área de notificación), existe una tercera opción: una aplicación residente que solo funciona mientras el usuario tiene la sesión iniciada.
¿Se puede mostrar una interfaz de usuario (pantalla) desde un servicio de Windows?
No es posible. Desde Windows Vista, los servicios se ejecutan en una sesión aislada llamada sesión 0 y no pueden interactuar directamente con el usuario. Aunque el servicio llame a MessageBox.Show, no aparece nada en la pantalla del usuario, y el proceso se queda esperando indefinidamente a que alguien pulse un botón Aceptar que nadie puede pulsar, lo que provoca un bloqueo (hang). Cuando se necesita interfaz de usuario, la solución correcta es separar el servicio y la aplicación de interfaz en dos procesos distintos que se comuniquen mediante comunicación entre procesos, por ejemplo con canalizaciones con nombre (named pipes). Al migrar código antiguo a un servicio, compruebe siempre que no queden cuadros de mensaje pensados para mostrar errores.
¿Cómo se crea un servicio de Windows en .NET?
La vía oficial consiste en partir de la plantilla Worker Service (dotnet new worker), agregar el paquete Microsoft.Extensions.Hosting.WindowsServices y llamar a AddWindowsService. El mismo ejecutable se comporta como una aplicación de consola normal al ejecutarlo con F5 en Visual Studio o con dotnet run, lo que facilita enormemente el desarrollo y la depuración. El registro se realiza con sc.exe create, pero hay que prestar atención a una peculiaridad: tras el signo igual de binpath= o start= es obligatorio dejar un espacio, y si se omite obj= la cuenta predeterminada es LocalSystem (el nivel de privilegio más alto). El directorio de trabajo actual al iniciar el servicio es C:\Windows\System32, por lo que los archivos de configuración deben resolverse en relación con el ejecutable.
¿Qué le ocurre al servicio si se produce una excepción dentro de BackgroundService?
Desde .NET 6, una excepción no controlada que escapa de ExecuteAsync se registra en el log y, de forma predeterminada (BackgroundServiceExceptionBehavior.StopHost), detiene el host. Sin embargo, esto se considera una «detención normal», por lo que las opciones de recuperación del SCM (reinicio automático) no se activan. Si desea que un fallo dispare el reinicio mediante las opciones de recuperación, debe finalizar el proceso de forma explícita con un código de salida distinto de 0, por ejemplo con Environment.Exit(1). Los fallos que se pueden continuar, como un corte de red, deben capturarse dentro del bucle, registrarse y reintentarse, diseñando el código para distinguirlos de los fallos irrecuperables.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog