El apagado de Windows visto desde la aplicación — sobrevivir correctamente a las notificaciones de cierre, los reinicios y los cortes de energía

· Actualizado el: · · Windows, Apagado, Desarrollo en Windows, Servicios de Windows, PC industrial, Protección de datos, Funcionamiento prolongado, SAI

Historial de revisiones (primera versión, publicada el 21 Aug 2026)
Primera publicación
Citar este artículo(DOI (archivo registrado): 10.5281/zenodo.22176521)

Los DOI siguientes remiten a versiones ya archivadas y pueden diferir del texto actual. Para citar el texto actual, utilice la URL de esta página.

Go Komura (2026). El apagado de Windows visto desde la aplicación — sobrevivir correctamente a las notificaciones de cierre, los reinicios y los cortes de energía. KomuraSoft LLC. https://comcomponent.com/es/blog/windows-shutdown-handling-for-apps/

DOI (archivo registrado)
10.5281/zenodo.22176521
DOI (última versión registrada)
10.5281/zenodo.22176522

«Un reinicio nocturno de Windows Update corrompió el archivo en el que estábamos midiendo.» «Al cerrar sesión en un PC compartido desaparece lo que estaba editando.» Para evitar este tipo de accidente, el apagado no debe tratarse como una excepción: que se pida desde fuera que la aplicación termine tiene que diseñarse como un comportamiento normal de la aplicación.

El código que recibe la notificación de cierre no basta por sí solo. El tiempo disponible tras la notificación es corto, y un corte de energía repentino no trae ninguna notificación. El eje de este artículo es hacer de el guardado habitual, un cierre breve y la recuperación en el siguiente arranque un todo continuo.

Dirigido a los responsables de sistemas de pequeñas y medianas empresas y a los desarrolladores de aplicaciones de Windows, en especial de PC industriales y aplicaciones de larga duración, este artículo organiza la elección de la ruta de notificación, la implementación, la reanudación tras un reinicio, la preparación ante un corte de energía y la verificación. La base técnica son las fuentes primarias de Microsoft Learn, a agosto de 2026, a las que se refería el artículo original.

1. Primero la conclusión — reducir lo que hay que guardar antes de esperar una notificación

Mantenga la aplicación en un estado en el que pueda terminar en unos segundos tras una notificación, y haga que el siguiente arranque pueda recuperar incluso cuando no llega ninguna notificación. Esa es la línea básica para el tratamiento del apagado.

En lugar de guardar un gran volumen de datos tras la notificación de cierre, guarde en cada hito del trabajo y mantenga pequeño el delta restante al terminar. Para las aplicaciones GUI y el cierre de consola, unos 5 segundos es la cifra clave. Los servicios tienen un margen distinto, pero ninguno es un tiempo que se tenga garantizado por completo.123

1.1. Elegir la ruta de notificación de su aplicación

Forma de la aplicación Dónde llega la petición de cierre Lo primero que hay que acertar Capítulo que leer
Aplicación GUI Win32 WM_QUERYENDSESSION y WM_ENDSESSION Responder a la consulta con TRUE de inmediato, por norma. Hacer el cierre tras el compromiso del fin en WM_ENDSESSION Capítulo 3
WinForms / WPF FormClosing / SessionEnding, más un gancho de mensaje si hace falta Ambos eventos pertenecen a la fase de consulta. Mover el cierre que no se puede deshacer a la notificación comprometida Capítulo 3
Aplicación de consola pura SetConsoleCtrlHandler y similares El cierre y el cierre de sesión/apagado se notifican en condiciones distintas Capítulo 5
Servicio de Windows SHUTDOWN / PRESHUTDOWN desde el SCM Declarar el indicador de controles aceptados y volver de inmediato del controlador de control Capítulo 6
Generic Host / Worker Service IHostApplicationLifetime y StopAsync Concentrar el cierre en la ruta de detención del Host y fijar ShutdownTimeout de forma explícita Capítulos 5 y 6

En .NET, no dependa solo de AppDomain.ProcessExit. Desde .NET 10, el runtime no ofrece un controlador predeterminado para CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. Esta ruta por señal externa es distinta de una salida normal como el retorno de Main. La sección 5.2 cubre los detalles.4

1.2. Fuera del tratamiento de notificaciones hacen falta dos preparativos

Cuando llega la notificación, no pregunte al usuario «¿Desea guardar?»; termine con un cierre breve y salga. El bloqueo temporal para un trabajo que de verdad no se puede interrumpir se trata en el capítulo 4, y la reanudación automática tras un reinicio en el capítulo 7.

El otro preparativo es dejar datos que aún se puedan leer tras un corte de energía que no trae notificación. El guardado en un archivo temporal, el vaciado, la sustitución, la copia de seguridad y la validación al arrancar se reúnen en el capítulo 8. Una vez implementada la ruta de notificación, continúe hasta el diseño de guardado y recuperación del capítulo 8 y la verificación del capítulo 9.

Un diseño de guardado común a la notificación de cierre y al corte de energíaEl guardado habitual mantiene pequeño el delta sin guardar; con notificación sigue un cierre breve, y sin ella los datos guardados se validan y recuperan antes de reanudar el trabajoSíNoGuardar en cada hito¿Hay notificación al terminar?Guardar solo el delta restante y terminarNo es posible un cierre en el momentoValidar y recuperar en el siguiente arranqueReanudar el trabajo

Figura 1: En lugar de esforzarse solo cuando llega la notificación, haga del guardado habitual hasta el siguiente arranque un proceso continuo.

En el diagrama, una línea continua marca una relación que siempre se cumple y una línea discontinua una relación condicional (las condiciones están en la explicación de cada relación en la página de detalle). La lista completa de relaciones (14 en total, con evidencia y grado de certeza) y las definiciones de los conceptos principales están reunidas en la página de detalle del mapa de conocimiento (en japonés). Datos: JSON-LD / Turtle

2. Distinguir primero — cierre de sesión, apagado, reinicio y corte de energía

2.1. Mirar por separado la sesión de usuario y el kernel

Dividir «qué termina» en dos partes facilita organizar el tratamiento. La sesión de usuario es el ámbito en el que se ejecutan las aplicaciones de ese usuario. El kernel y los controladores, en cambio, pertenecen al lado del sistema operativo y siguen en marcha tras el cierre de sesión.

Operación Sesión de usuario Kernel y controladores Notificaciones
Cierre de sesión Termina Sigue en marcha La GUI recibe la consulta de cierre y la notificación comprometida. Los servicios no se detienen al cerrar sesión
Apagado con inicio rápido activado Termina Guarda el estado de hibernación en hiberfil.sys Las notificaciones de fin de sesión de la GUI y la notificación de apagado a los servicios
Reinicio Termina Termina por completo y hace un arranque completo la próxima vez Igual que arriba
Corte de energía repentino Se pierde al instante Se pierde al instante No hay notificación

En el WM_QUERYENDSESSION de la GUI, el bit ENDSESSION_LOGOFF de lParam indica un cierre de sesión. Si lParam es 0, se trata de un apagado o un reinicio, y no se pueden distinguir. Trate lParam como una máscara de bits.1

Para los datos no guardados de la aplicación, el cierre de sesión y el apagado exigen la misma preparación. No los separe en «en un cierre de sesión no hace falta guardar»; lleve ambos a una rutina de guardado común. Eso no hace idénticas las condiciones de notificación de consola y servicios; compruebe las rutas de los capítulos 5 y 6 para cada uno.

Pensar el cierre de sesión y la salida del sistema por separadoEl cierre de sesión termina las aplicaciones del usuario mientras los servicios siguen; un apagado o reinicio del sistema también detiene servicios; un corte de energía no notifica a ningunoCierre de sesiónLas aplicaciones del usuario terminanLos servicios siguen en marchaApagado o reinicioLos servicios también se detienenCorte de energíaSin notificación a ninguno

Figura 2: Un cierre de sesión permite ejercitar el tratamiento de cierre de la GUI, pero no cuenta como prueba del tratamiento de detención de un servicio.

2.2. Por qué «el apagado no lo arregla, el reinicio sí»

En los sistemas operativos cliente a partir de Windows 8, el inicio rápido está activado de forma predeterminada en muchos PC que admiten hibernación. En esta configuración, un apagado cierra la sesión del usuario, pero el estado del kernel y de los controladores de dispositivo se guarda en el archivo de hibernación y se restaura en el siguiente arranque. Cortar la alimentación no significa necesariamente que se haya restablecido todo el estado del sistema operativo.5

Este comportamiento es condicional. Donde la hibernación está desactivada (powercfg /hibernate off), donde el inicio rápido está apagado por directiva o en Opciones de energía, y en Windows Server, se obtiene el apagado completo tradicional. Compruebe la configuración de Opciones de energía y use powercfg /a para ver si el inicio rápido está disponible.

«Reiniciar», en cambio, siempre ejecuta un ciclo de arranque completo. En un procedimiento para aislar un problema de controlador, indique «reiniciar» de forma explícita, no «apagar y volver a encender».5

Inicio rápido frente a un arranque completoUn apagado con inicio rápido activado guarda y restaura el estado del kernel y de los controladores, mientras que un apagado completo o un reinicio inicializa todo con un arranque completoSíNoApagado¿Usar el inicio rápido?Hibernar el kernel y los controladoresRestaurar el estado en el siguiente arranqueApagado completoInicializar con un arranque completo la próxima vezReinicio

Figura 3: Cortar la alimentación no es lo mismo que restablecer el kernel y los controladores.

Desde la línea de comandos, shutdown /s pide un apagado completo explícito y shutdown /s /hybrid el comportamiento híbrido. Shutdown.exe tiene como valor predeterminado un apagado completo, así que es importante no dar por hecho que es lo mismo que el elemento «Apagar» en pantalla.5

Desactivar el inicio rápido como solución alternativa no se recomienda. Construya la aplicación para que funcione tanto si está activado como si no. Por ejemplo, no estime el «tiempo de funcionamiento acumulado» de un equipo solo a partir de la hora de arranque del sistema operativo; incorpore en el diseño que el estado del kernel puede arrastrarse.

3. Aplicaciones GUI — separar la consulta del cierre comprometido

3.1. WM_QUERYENDSESSION es la pregunta, WM_ENDSESSION es el resultado

Una aplicación que tiene una ventana y una cola de mensajes recibe la notificación del fin de sesión en dos etapas.1

Mensaje Significado Qué hacer
WM_QUERYENDSESSION La consulta «¿Se puede terminar?» Devolver TRUE de inmediato, por norma. DefWindowProc también devuelve TRUE de forma predeterminada
WM_ENDSESSION, wParam=TRUE El fin de la sesión está comprometido Hacer el cierre breve: guardar, desconectar, etc.
WM_ENDSESSION, wParam=FALSE El fin de la sesión se canceló La aplicación sigue en marcha. No hacer el cierre que solo es posible tras el compromiso del fin

Devolver TRUE a la consulta uno mismo no significa que el cierre sea seguro. Otra aplicación puede rechazar, y el cierre se cancela. Si en ese punto desconecta o cede recursos que necesita, una aplicación que no terminó ya no puede trabajar. Por eso el cierre se mueve a la notificación comprometida.1

La consulta de la GUI y el resultado del cierreIncluso después de TRUE a WM_QUERYENDSESSION otra aplicación puede cancelar el cierre, así que el cierre posterior al compromiso solo se ejecuta cuando wParam de WM_ENDSESSION es TRUESíNoWM_QUERYENDSESSIONDevolver TRUE de inmediato, por normaWM_ENDSESSION¿wParam es TRUE?Cierre posterior al compromisoCierre cancelado; seguir en marcha

Figura 4: No confunda la respuesta que permite el cierre con la notificación de que el cierre está comprometido.

Hay casos en los que se puede devolver FALSE para rechazar el cierre, pero la norma es respetar la intención del usuario de terminar. Una aplicación que rechaza se muestra como una aplicación que impide el apagado. Las aplicaciones de consola y las que no tienen ventana visible también tienen restricciones: en una configuración ordinaria, una aplicación que no responde en 5 segundos puede terminarse automáticamente. Trate el bloqueo como el tratamiento excepcional del capítulo 4 y no lo use para el guardado ordinario.6

3.2. Unos 5 segundos no son una garantía de que se pueda terminar de guardar

Si retrasa la respuesta unos 5 segundos en la etapa WM_QUERYENDSESSION o WM_ENDSESSION, el sistema muestra la pantalla de las aplicaciones que impiden el apagado, y el usuario puede elegir forzar el apagado. Tras una terminación forzada no hay ocasión de terminar el resto del guardado.6

La contramedida es guardar de forma habitual y reducir el delta al terminar. Deje el estado de trabajo no guardado en un lugar temporal y restáurelo en el siguiente arranque. No diseñe la aplicación para mostrar un cuadro de confirmación durante el apagado y esperar. Trate la confirmación de cierre ordinaria y una petición de cierre del sistema operativo como cosas distintas.1

Mantener pequeño el trabajo al terminarUn diseño que guarda de forma habitual deja un delta pequeño al terminar, mientras que un diseño que acumula en memoria hasta el cierre no cabe en el margen breve y corre riesgo de pérdida por terminación forzadaGuardar de forma habitualEl delta restante al terminar es pequeñoTerminar con un cierre breveAcumular en memoria hasta el cierreGuardarlo todo al terminarPoco margen; riesgo de terminación forzada

Figura 5: La preparación para terminar en unos segundos se hace antes de la notificación de cierre.

3.3. En WinForms y WPF, separar el guardado del cierre que no se puede deshacer

En WinForms el equivalente es FormClosing con CloseReason.WindowsShutDown; en WPF es Application.SessionEnding. WPF también puede registrarlo con el atributo SessionEnding en XAML, y se puede tratar redefiniendo OnSessionEnding.

Ambos, no obstante, son eventos de la fase de consulta. Lo que se permite aquí es, como máximo, un «guardado de instantánea idempotente»: inofensivo si se cancela el cierre, y con el mismo resultado por muchas veces que se ejecute. El trabajo que solo es posible tras el compromiso del fin, como desconectar, se hace recibiendo WM_ENDSESSION con wParam=TRUE en el WndProc de WinForms o un gancho de WPF.

Reparto del tratamiento de cierre en WinForms y WPFFormClosing y SessionEnding guardan una instantánea segura incluso si se cancela el cierre, y un gancho sobre WM_ENDSESSION con TRUE hace el cierre que no se puede deshacerFase de consultaFormClosingSessionEndingGuardado de instantánea idempotenteWM_ENDSESSION con TRUERecibir en WndProc o un ganchoTrabajo posterior al compromiso, como desconectar

Figura 6: No meta el trabajo posterior al compromiso en los eventos del marco.

Los dos ejemplos siguientes son la parte que guarda solo el estado de trabajo en la etapa de consulta. Los fragmentos de código son extractos de implementación; el contenido de la función de guardado, el tratamiento de fallos, el registro de eventos, etc. corresponden a la aplicación.

// WinForms: FormClosing también se llama en el apagado y el cierre de sesión
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // Solo un guardado de instantánea idempotente. No mostrar ningún cuadro.
        // Tampoco establecer e.Cancel = true (rechazar).
        SaveWorkingStateToTempFile();
        return;
    }

    // En casos normales, como cuando el usuario cierra con el botón X, confirmar aquí está bien
}
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // ReasonSessionEnding.Logoff / Shutdown se pueden distinguir, pero
    // el enfoque básico es ejecutar el mismo guardado de instantánea para ambos
    SaveWorkingStateToTempFile();

    // No establecer e.Cancel = true salvo una razón muy sólida
}

En ambos casos, lleve los datos usados en el cierre normal, en el apagado y, si es posible, en un bloqueo a una función y un formato de guardado comunes. Si evita crear un formato distinto para cada ruta de guardado, la lógica de restauración en el siguiente arranque también puede ser una sola. El diseño para dejar información en un bloqueo se trata en Diseño para conservar registros y volcados de memoria cuando falla una aplicación de Windows.

4. Bloquear de forma temporal, y solo para un trabajo que no se puede interrumpir

4.1. Registrar un motivo y rechazar el cierre son trabajos distintos

El trabajo que se destruye físicamente si se interrumpe, como grabar un CD o escribir firmware, es la excepción. Registre un motivo con ShutdownBlockReasonCreate al comenzar el trabajo y elimínelo con ShutdownBlockReasonDestroy al terminarlo. El motivo registrado se muestra en la pantalla de las aplicaciones que impiden el apagado.7

Tenga en cuenta, no obstante, que registrar una cadena de motivo por sí sola no detiene el apagado. Combínela con un indicador de «protegido» y, solo mientras esté activo, devuelva FALSE a WM_QUERYENDSESSION. Cuando el trabajo termina, quite tanto el motivo como el indicador de protección.

Reparto de roles en un bloqueo temporal de cierreSolo mientras corre un trabajo ininterrumpible, combinar el registro del motivo y el rechazo de la consulta, y quitar ambos al terminar; un apagado forzado por el usuario sigue sin poder evitarseCancelarForzarEmpieza el trabajo ininterrumpibleRegistrar el motivo y activar el indicador de protecciónProcesar en un trabajadorTerminar; quitar el motivo y el indicadorLlega una consulta de cierre entretantoDevolver FALSE y mostrar el motivoDecisión del usuarioSeguir en marchaPuede terminarse

Figura 7: Registrar un motivo no es un rechazo, e incluso implementar el rechazo no evita un apagado forzado.

4.2. Mantener el hilo de UI en un estado en el que pueda recibir la petición de cierre

Registre y quite el motivo desde el hilo que creó la ventana de destino. Las llamadas desde otros hilos fallan.7 El trabajo largo ininterrumpible en sí, en cambio, pasa a un hilo de trabajo. Si el hilo de UI queda bloqueado por un procesamiento síncrono, la aplicación pasa a «No responde» antes de que se procese el mensaje necesario para rechazar.

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);

[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);

// Llamar desde el hilo que creó la ventana principal (las llamadas desde otros hilos fallan)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Escribiendo los datos de medición en un archivo");
try
{
    // Ejecutar el trabajo ininterrumpible en un hilo de trabajo. Ejecutarlo de forma síncrona
    // en el hilo de UI detiene la bomba de mensajes, y la aplicación se fuerza como
    // «No responde» antes de que pueda ejecutarse el código de rechazo de WM_QUERYENDSESSION de abajo
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// Además, devolver FALSE a WM_QUERYENDSESSION solo mientras está protegido, para rechazar
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // Rechazar. La cadena de motivo registrada se muestra en la IU a pantalla completa
        return;
    }
    base.WndProc(ref m);
}

Este ejemplo es el esqueleto que combina el registro del motivo, el procesamiento en un trabajador y el rechazo de la consulta. El tratamiento de errores, incluidos los fallos de API, hace falta por separado. Mantenga la cadena de motivo corta y concreta, y limite el registro al tiempo en que el trabajo de verdad no se puede interrumpir, en lugar de dejarlo registrado durante toda la vida de la aplicación.7

Aun así, el usuario puede elegir forzar el apagado. También hay rutas de cierre forzado como ENDSESSION_CRITICAL, así que nunca haga de poder bloquear una premisa de la integridad de los datos. La preparación para el caso en que no pudo detenerlo es el diseño de guardado y recuperación del capítulo 8.6

5. Consola y .NET — comprobar las condiciones en las que llegan las notificaciones

5.1. Notificaciones y márgenes recibidos con SetConsoleCtrlHandler

En una aplicación de consola, las señales de control llegan al controlador registrado con SetConsoleCtrlHandler. A diferencia del tratamiento de mensajes de la GUI, el controlador se ejecuta en un hilo distinto.2

Señal Cuándo ocurre Margen predeterminado
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break Sin tiempo de espera explícito
CTRL_CLOSE_EVENT Cerrar la consola, «Finalizar tarea» en el Administrador de tareas, etc. Unos 5 segundos
CTRL_SHUTDOWN_EVENT Procesos de servicio en el apagado del sistema Unos 20 segundos

Terminar a la fuerza un proceso desde la pestaña «Detalles» del Administrador de tareas, por ejemplo, es una terminación inmediata sin notificación, y queda fuera de esta tabla.2

No diseñe una aplicación de consola en una sesión interactiva para esperar CTRL_LOGOFF_EVENT o CTRL_SHUTDOWN_EVENT. Las aplicaciones interactivas se terminan en el momento del cierre de sesión, así que en la práctica solo los procesos que se ejecutan como servicio pueden recibirlas. Además, un proceso que carga gdi32.dll o user32.dll se trata como una aplicación de Windows, y no se llaman los controladores de LOGOFF/SHUTDOWN. La alternativa oficial en ese caso es crear una ventana oculta y recibir WM_QUERYENDSESSION / WM_ENDSESSION.28

Condiciones para recibir las notificaciones de cierre de la consolaUna consola interactiva puede recibir la notificación de cierre pero no puede contar con notificaciones de cierre de sesión o apagado, y hasta un servicio necesita otra ruta de notificación una vez que carga las DLL de GUISesión interactivaServicioNoSíProceso de consolaEl cierre va al controlador de control¿Esperar LOGOFF o SHUTDOWN?No se puede confiar en esta notificación¿Se cargaron las DLL de GUI?Tratar la señal de control correspondienteRecibir mediante una ventana oculta

Figura 8: No trate el manejo del cierre de consola como el manejo del apagado.

El siguiente extracto se prepara para Ctrl+C y el cierre de consola. Conserva el delegado para que el recolector de basura no lo recoja y, tras un cierre breve, sigue hacia el controlador predeterminado. Registrar esto solo no garantiza las notificaciones de apagado para una aplicación interactiva.

// Aplicación de consola: cerrar en Ctrl+C y al cerrar la consola
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

delegate bool HandlerRoutine(int ctrlType);   // 2 = CTRL_CLOSE_EVENT

static readonly HandlerRoutine s_handler = OnCtrlEvent;  // Conservar una referencia para que el GC no lo recoja

static bool OnCtrlEvent(int ctrlType)
{
    // Solo el cierre que termina en 5 segundos
    FlushAndCloseDataFile();
    return false;   // Seguir al controlador predeterminado; el proceso termina
}

static void Main()
{
    SetConsoleCtrlHandler(s_handler, add: true);
    // ...
}

5.2. Desde .NET 10, no poner el cierre solo en ProcessExit

Desde .NET 10, el runtime ya no ofrece un controlador predeterminado para CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. Sin un controlador propio o de una biblioteca de nivel superior, el tratamiento predeterminado del sistema operativo termina el proceso, y en esta ruta no se disparan ni AppDomain.ProcessExit ni AssemblyLoadContext.Unloading.4

Esto es un cambio del comportamiento predeterminado ante señales de terminación externas. No significa que ProcessExit deje de dispararse en una salida normal como el retorno de Main.

Quitar la dependencia del controlador de terminación predeterminado de .NETEl runtime anterior conectaba las señales correspondientes con ProcessExit, pero desde .NET 10 no hay controlador predeterminado, así que use la ruta de notificación del modelo de aplicaciónNoSíSeñal CLOSE o SHUTDOWNTratamiento predeterminado del runtime anteriorDispara ProcessExit y similaresSin tratamiento predeterminado desde .NET 10¿La aplicación lo trata?Terminado por el tratamiento predeterminado del SOCierre que encaja con el modelo

Figura 9: Distinga una salida normal de una terminación por señal externa, y proporcione el controlador que necesita su modelo de aplicación.

Concentre el cierre donde encaje con el modelo de aplicación. Para una GUI, son los eventos y la notificación comprometida del capítulo 3; para Generic Host, IHostApplicationLifetime y BackgroundService.StopAsync; para una aplicación de consola pura, SetConsoleCtrlHandler o PosixSignalRegistration. Si usa este último, elija las señales que correspondan a las rutas de terminación que cubre, como SIGINT, SIGTERM y SIGHUP.4

En Generic Host, fije HostOptions.ShutdownTimeout de forma explícita. El ajuste del lado del Host, no obstante, no amplía por sí solo el margen exterior de la GUI, la consola o el SCM. Aunque Ctrl+C no tenga un tiempo de espera explícito, sigue haciendo falta prepararse para un corte de energía y otras rutas de terminación. En todos los modelos la línea es la misma: mantener el estado guardado y mantener pequeño el trabajo tras la notificación.

El procesamiento de detención del Host y el margen de cierre exteriorLa detención de Generic Host se concentra en StopAsync, pero fijar el tiempo de espera de HostOptions no amplía el margen de cierre del SO o del SCM, así que compruebe ambosPetición de cierre del SO o del SCMRuta de detención del HostCerrar en StopAsyncFijar el tiempo de detención del HostEl margen exterior existe por separadoMedir si termina en poco tiempo

Figura 10: Compruebe por separado los límites de tiempo del Host y del sistema operativo, y no se conforme con ampliar el ajuste.

6. Servicios de Windows — volver de inmediato del controlador de control

6.1. La diferencia entre SHUTDOWN y PRESHUTDOWN

Los servicios no se detienen al cerrar sesión, pero sí se detienen en el apagado y el reinicio. La notificación llega del SCM (Administrador de control de servicios), y el servicio debe declarar el indicador que corresponde al código de control que quiere recibir.3

Indicador que declarar Código de control entregado Cuándo usarlo
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN La notificación de apagado ordinaria. El margen predeterminado es de unos 20 segundos y depende de WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN Se entrega antes del SHUTDOWN ordinario. El SCM espera a que el servicio se detenga o expire el tiempo configurado
Etapas de la notificación de cierre a los serviciosEl SCM notifica primero a los servicios que aceptan PRESHUTDOWN, espera a que se detengan o expiren y luego sigue hacia la notificación SHUTDOWN ordinariaEmpieza el apagado del sistemaNotificar a los servicios que aceptan PRESHUTDOWNEsperar la detención o el plazo configuradoNotificar a los servicios que aceptan SHUTDOWNTras el margen, seguir hacia la salida del sistema

Figura 11: Recibir la notificación pronto y que se le espere son condiciones distintas; PRESHUTDOWN también tiene un plazo configurado.

El tiempo de espera de PRESHUTDOWN se configura con ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO). El valor predeterminado es 10 segundos desde Windows 10 Creators Update (compilación 15063) en adelante y 3 minutos antes. Si asume «PRESHUTDOWN siempre compra 3 minutos», no obtendrá el tiempo que esperaba.9

Como PRESHUTDOWN retiene el apagado de todo el sistema, úselo solo cuando sea realmente necesario. Reescribir el WaitToKillServiceTimeout ordinario desde el servicio para ampliarlo tampoco se recomienda.3

6.2. Separar la recepción de la notificación de la detención real

El controlador de control debe volver en 30 segundos, pero en lugar de pensarlo como 30 segundos que se pueden usar, estructúrelo para señalar la detención y volver de inmediato. Entregue el trabajo largo a otro hilo e informe SERVICE_STOP_PENDING.3

// Servicio Win32: aceptar PRESHUTDOWN y dejar el trabajo de detención a un trabajador
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;

DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
    switch (control)
    {
    case SERVICE_CONTROL_PRESHUTDOWN:
    case SERVICE_CONTROL_STOP:
        ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
        SetEvent(g_stopEvent);   // Señalar al trabajador que se detenga y volver de inmediato
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// Lado del trabajador: si el cierre dura más que la indicación de espera, seguir
// informando SERVICE_STOP_PENDING periódicamente e incrementando dwCheckPoint. El SCM
// juzga por la indicación de espera y el punto de comprobación que avanza que el servicio
// «sigue vivo y avanza». Si los informes se detienen, se trata como colgado y el apagado
// puede seguir. Informe siempre SERVICE_STOPPED al terminar

En el lado del trabajador, si el cierre dura más que la indicación de espera, siga informando SERVICE_STOP_PENDING mientras avanza dwCheckPoint. Si los informes de progreso se detienen, el servicio puede juzgarse colgado. Informar SERVICE_STOPPED al completar forma parte del procesamiento de detención.39

Reparto del trabajo entre el controlador de control del servicio y el trabajadorEl controlador de control informa detención pendiente, señala al trabajador y vuelve de inmediato; el trabajador hace el cierre y los informes de progreso y al final informa detenidoControlador de controlInformar STOP_PENDINGSeñalar al trabajador que se detengaEl controlador vuelve de inmediatoEl trabajador cierraInformar el progreso si tardaInformar STOPPED al completar

Figura 12: No bloquee el hilo que recibe la notificación con un procesamiento de detención largo.

6.3. Poder terminar en poco tiempo aunque una dependencia se detenga primero

En el apagado, el SCM envía las notificaciones de forma predeterminada sin tener en cuenta las dependencias de servicios. El procesamiento de detención debe tratar el caso en que un servicio de dependencia ya no está disponible, y no debe esperar demasiado las respuestas de los pares de red. En lugar de gastar tiempo en liberar memoria y similares, dé prioridad a confirmar con rapidez los datos necesarios.3

Esta línea también importa en relación con un SAI. Cuanto más alarga un servicio su espera, más difícil es completar el apagado de todo el sistema operativo antes de que se agote la batería. En lugar de aumentar el margen, reduzca el trabajo en la detención guardando en cada hito.3

Cuando un Worker Service de .NET se ejecuta con UseWindowsService, STOP / SHUTDOWN se convierten en una detención del Host, que lleva a BackgroundService.StopAsync. La implementación estándar, en el momento de redacción del artículo original, no acepta PRESHUTDOWN, así que si lo necesita hay que ampliar la implementación del controlador. La línea de fijar HostOptions.ShutdownTimeout de forma explícita y terminar el propio StopAsync en unos segundos es la misma. Para la implementación general, véase Cómo crear y operar un servicio de Windows.

7. Reanudación tras un reinicio — alinear el registro, los datos de restauración y el inicio de sesión

7.1. RegisterApplicationRestart preregistra la ruta de reanudación

En los PC industriales y en el funcionamiento desatendido, el diseño va más allá de guardar y terminar: cubre reanudar el trabajo tras el reinicio. RegisterApplicationRestart es la API que registra la aplicación para el reinicio en cada una de estas situaciones: un bloqueo (una excepción no controlada), no responde, un reinicio de la aplicación causado por una actualización y un reinicio del sistema operativo causado por una actualización.10

Se pueden indicar argumentos de línea de comandos para el reinicio, como los archivos que estaban abiertos o un punto de restauración. Registrarse solo, no obstante, no significa una recuperación automática desde cualquier situación.

Punto que comprobar Condición o restricción
Cuándo registrarse Antes de que ocurra un problema. En el escenario de actualización, el tratamiento de WM_QUERYENDSESSION es la última oportunidad
Prevención de un bucle de reinicio Un proceso que lleva menos de 60 segundos en ejecución no se reinicia
Bloqueo o cuelgue Se reinicia tras el consentimiento del usuario. Un reinicio causado por una actualización es automático
A través de un reinicio del sistema operativo El lado que inicia debe llamar a la API de apagado con EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Proceso con privilegios elevados No es elegible para el reinicio automático, así que hace falta una ruta de inicio explícita aparte

Para una aplicación que necesita elevación, diseñe la ruta de reanudación ejecutando la IU con privilegios estándar y separando el trabajo privilegiado en un servicio, o usando una tarea del Programador de tareas configurada en «Ejecutar con los privilegios más altos» o similar.10

7.2. La devolución de llamada de recuperación y ARSO tienen papeles distintos

Si también usa RegisterApplicationRecoveryCallback, WER (Informe de errores de Windows) llama a la devolución de llamada de recuperación en un bloqueo y se pueden guardar los datos en los que se estaba trabajando. Mientras continúa el guardado, llame a ApplicationRecoveryInProgress dentro del intervalo de ping registrado, y notifique ApplicationRecoveryFinished al completar. Si las notificaciones de progreso se detienen, el procesamiento de recuperación puede cortarse.

Guardado y notificación de progreso en la devolución de llamada de recuperaciónLa devolución de llamada de recuperación invocada por WER sigue llamando a ApplicationRecoveryInProgress dentro del intervalo de ping mientras guarda, y notifica ApplicationRecoveryFinished al completarAún noHechoPreregistrar la devolución de llamada de recuperaciónBloqueo; WER la invocaGuardar los datos en los que se trabajaInformar el progreso dentro del intervalo de ping¿Guardado completo?Notificar la recuperación terminadaPuede cortarse si los informes se detienen

Figura 13: No se limite a guardar; notifique a WER el progreso y la finalización.

Sustituir archivos en uso durante una actualización y reiniciar es el trabajo de Restart Manager. Se trata en Cómo reemplazar un exe o una DLL en uso.

El mecanismo que devuelve la sesión de usuario tras un reinicio del sistema operativo, en cambio, es ARSO (inicio de sesión automático al reiniciar de Winlogon). Cuando Windows Update inicia un reinicio automático, guarda de forma segura las credenciales del último usuario interactivo y configura Autologon, y tras el reinicio inicia la sesión de ese usuario y bloquea la pantalla.11

Registro de reinicio, inicio de sesión y restauración de datosFrente a un registro previo de reinicio, un bloqueo exige el consentimiento del usuario, y las aplicaciones de usuario tras un reinicio del SO necesitan la sesión restaurada mediante ARSO o similar y el estado guardado restauradoRegistrarse para el reinicio antes de que ocurra un problemaBloqueo o no respondeObtener el consentimiento del usuarioReiniciar la aplicaciónReinicio del SO con los indicadores requeridosRestaurar la sesión mediante ARSO o similarLeer el punto de restauración guardadoComprobar la directiva y las condiciones de arranque

Figura 14: Proporcione no solo el registro de reinicio de la aplicación, sino también la ruta por la que vuelven la sesión y el estado de trabajo.

shutdown /g es el comando que pide un reinicio más la reanudación de las aplicaciones registradas. ARSO puede estar desactivado por una directiva de la organización como DisableAutomaticRestartSignOn, así que compruébelo junto con los requisitos de la reanudación desatendida. El procesamiento en segundo plano que siempre hace falta se ejecuta mejor como servicio de Windows que dependiendo del inicio de sesión automático del usuario.11

8. Corte de energía sin notificación — diseñar el guardado y la carga como un par

8.1. Archivo temporal, sustitución, copia de seguridad y validación al arrancar como un conjunto

Un disyuntor disparado, una fuente de alimentación averiada o un enchufe arrancado no traen ni WM_ENDSESSION ni PRESHUTDOWN. Si sobrescribe el archivo original en el sitio, una interrupción a mitad de camino puede dejar un archivo con contenidos antiguos y nuevos mezclados.

Lo básico es escribirlo todo en un archivo temporal del mismo volumen, vaciarlo y después sustituirlo. ReplaceFile agrupa los pasos que corresponden a guardar en un archivo nuevo, apartar el original, cambiar el nombre y eliminar, y transfiere atributos como la fecha de creación, la ACL y los flujos de datos alternativos. El archivo sustituido, el de sustitución y la copia de seguridad deben estar en el mismo volumen. File.Replace de .NET llama a esta API.12

Guardar un archivo y recuperar en el siguiente arranqueEscribir en un archivo temporal del mismo volumen, vaciar, sustituir conservando una copia de seguridad, luego validar el archivo principal al arrancar y recurrir a la copia de seguridad si hace faltaSíNoArchivo temporal en el mismo volumenEscribir hasta el final y vaciarSustituir; el contenido antiguo va a .bakSiguiente arranque¿El archivo principal está intacto?Leer el archivo principalRecurrir a la copia de seguridad

Figura 15: Implemente no solo la forma segura de escribir, sino también la forma de leer cuando el archivo está dañado.

// El patrón estándar para guardar configuración y datos: escribir en un archivo temporal, luego sustituir, conservando el contenido antiguo
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // Crearlo en el mismo volumen

    try
    {
        using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
            writer.Flush();
            fs.Flush(flushToDisk: true);   // Equivalente a FlushFileBuffers. Escribe los búferes del SO
                                           // al disco (los límites de la caché del dispositivo están en la sección 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // Llama a ReplaceFile. Conserva el contenido antiguo como .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // Si falla a mitad de camino, no dejar el archivo temporal. Si los guardados
        // periódicos siguen fallando, copias completas irían llenando el volumen
        try { File.Delete(tmp); } catch { /* Un borrado fallido cede a la excepción original */ }
        throw;
    }
}

Aunque este ejemplo se llame SaveAtomically, no garantiza atomicidad a través de un corte de energía. En el funcionamiento normal deja legible el archivo antiguo completo o el nuevo completo, pero ReplaceFile es una operación de espacio de nombres en varios pasos, y su atomicidad frente a un corte de energía repentino no está garantizada por la especificación. Precisamente por eso se conserva el .bak y se implementa la rutina de carga que valida el archivo principal al arrancar y recurre a la copia de seguridad si está dañado.12

El catch del ejemplo existe para que los archivos temporales no se acumulen en fallos ordinarios y llenen el volumen. No espera que este código se ejecute en el momento de un corte de energía. Para registros y CSV de solo anexión, no aplique el intercambio de archivo completo tal cual; use un formato que tenga en cuenta cómo se rompe, por ejemplo «escribir una línea por registro y descartar una última línea dañada al cargar».

8.2. Distinguir el éxito de WriteFile de la llegada al disco

Aunque WriteFile tenga éxito, los datos pueden seguir en la caché del sistema operativo. Windows suele escribir en los búferes del sistema y aplicar los datos al disco con escritura diferida. En los hitos importantes, vacíe con FlushFileBuffers, o especifique FILE_FLAG_WRITE_THROUGH en CreateFile para pedir una escritura inmediata. Los metadatos del sistema de archivos también se almacenan en caché, así que confirmarlos implica un vaciado o write-through.13

Write-through, no obstante, no es lo mismo que «no usar la caché del sistema operativo». Las llamadas frecuentes a FlushFileBuffers son ineficaces, así que considere combinarlo con FILE_FLAG_NO_BUFFERING cuando haga falta. En la práctica, el diseño realista es vaciar en los puntos que importan para la integridad, como los límites de transacción y justo antes de cerrar el archivo.13

El límite entre el éxito de escritura y la persistenciaUna escritura ordinaria llega al almacenamiento desde la caché del SO con retraso; un vaciado o write-through empuja a confirmar, pero quedan las restricciones de la caché del dispositivoWriteFile tiene éxitoPuede estar en la caché del SOEscritura diferidaVaciar en un hitoAplicado al almacenamientoLímites de la caché del dispositivo

Figura 16: No trate el éxito de la API, la confirmación de la caché del sistema operativo y la resistencia a un corte de energía como la misma cosa.

La caché volátil del lado del dispositivo también tiene límites, y no se puede decir «vaciamos, así que sobrevive por completo a un corte de energía en cualquier hardware». La relación entre el administrador de caché, la escritura diferida y las cachés de hardware se explica con detalle en Las profundidades de la E/S de Windows (parte 4) — El administrador de caché: ¿cuándo llega su WriteFile al disco?.

8.3. Usar el SAI para convertir un corte de energía en un apagado planificado

El papel de un SAI no es eliminar los apagones, sino convertir un corte de energía sin notificación en un apagado planificado con notificación. La condición que hace falta es esta relación:

Autonomía de la batería del SAI > tiempo para detectar el cambio + cierre de aplicaciones y servicios + finalización del apagado del sistema operativo

El cambio entre alimentación de CA y batería, y una carga restante baja, se comunican mediante PBT_APMPOWERSTATUSCHANGE. Una GUI lo recibe por WM_POWERBROADCAST; un servicio declara SERVICE_ACCEPT_POWEREVENT y después recibe SERVICE_CONTROL_POWEREVENT en HandlerEx. WM_POWERBROADCAST no se entrega al controlador de control de un servicio.14

Tras recibirlo, compruebe ACLineStatus y BatteryLifePercent con GetSystemPowerStatus, y continúe hacia la suspensión de la medición, el guardado y la petición de apagado.14

De la detección del SAI a la finalización del apagadoDetectar el paso del SAI a batería por la ruta de notificación adecuada, comprobar el estado de energía, seguir hacia el guardado y el apagado, y diseñar toda la secuencia para que quepa en la autonomíaApagón; el SAI pasa a bateríaNotificación de energía en la ruta correspondienteComprobar el estado de energía y la carga restanteSuspender la medición y guardarPedir un apagado al sistema operativoCompletar el cierre y la salida del SOHacer caber toda la secuencia en la autonomía

Figura 17: Haga caber no solo la aplicación, sino el tiempo hasta que el sistema operativo termine, en la autonomía del SAI.

En configuraciones en las que Windows ve el SAI como una batería, como un SAI USB típico, las API estándar pueden supervisarlo. Si el software de gestión del proveedor tiene una función «apagar con N % restante», compruebe también que su umbral sea coherente con el tiempo de cierre. Reanudar desde suspensión o hibernación es otro eje; véase Suspensión, hibernación y Modern Standby: evitar con diseño que las apps de larga duración se detengan de noche.

9. Verificación — confirmar las notificaciones, el tiempo y el resultado de la recuperación

9.1. Reproducir las rutas de cierre en una máquina de prueba, no en producción

No pruebe primero en el PC industrial de producción; use una máquina de prueba o una máquina virtual como Hyper-V. En una máquina virtual, tome un punto de control de antemano y repita las pruebas con datos de prueba que se pueda permitir perder.

Operación que probar Qué confirmar
Cierre de sesión El WM_QUERYENDSESSION → WM_ENDSESSION de la GUI y el procesamiento de guardado. No sustituye la verificación de la detención de un servicio
shutdown /s /t 0 El comportamiento en un apagado completo
shutdown /s /hybrid /t 0 El comportamiento híbrido en una configuración que usa inicio rápido
shutdown /r /t 0 Un reinicio con arranque completo, y la reanudación después
Apagar la máquina virtual Si el siguiente arranque recupera aunque el SO invitado se detenga sin notificación
Corte de energía en hardware equivalente al de producción La resistencia incluyendo el almacenamiento físico y el controlador

Un cierre de sesión se diferencia en que se activa el bit ENDSESSION_LOGOFF, pero es una forma fácil de confirmar la ruta de notificación de la GUI. No confunda los comandos de apagado completo, híbrido y reinicio; pruébelos por separado.15

Ampliar la verificación del apagado por etapasProbar las notificaciones de GUI y cada operación de cierre en el entorno de prueba, confirmar la recuperación tras una parada súbita en una MV y luego verificar la resistencia a un corte incluyendo el almacenamiento en hardware equivalente al de producciónPreparar la máquina de prueba y los datosProbar las rutas de notificación y las operaciones de cierreMedir el tiempo de cierreConfirmar la recuperación tras una parada súbita de la MVVerificar en hardware real incluyendo el almacenamientoConfirmar los datos y la reanudación en el siguiente arranque

Figura 18: Pruebe no solo si la aplicación termina con normalidad, sino también qué puede recuperar tras una parada súbita.

Apagar una máquina virtual solo reproduce que el invitado se detiene sin aviso. No puede reproducir la pérdida de la caché volátil de un disco físico ni una corrupción dependiente del controlador, así que al enviar como PC industrial, haga la confirmación final en hardware equivalente al de producción.

Registre una marca de tiempo al inicio y al final de la función de cierre, y mida si cabe en los unos 5 segundos de una GUI y similares, o en el margen configurado para el servicio. Confirme no solo que el cierre tuvo éxito, sino también qué se cargó en el siguiente arranque y desde dónde se pudo reanudar el trabajo.

9.2. Aislar lo que ocurrió por la noche a partir del registro de eventos

En el registro Sistema de Windows, el identificador de evento 1074 registra el proceso que inició el apagado, el usuario y el motivo. En un apagado inesperado, en el siguiente arranque se registra 41 (Kernel-Power) o 6008.15

# Comprobar el historial reciente de eventos relacionados con el apagado
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

Si 1074 muestra un reinicio de Windows Update y los datos aun así estaban dañados, investigue primero el tratamiento de la notificación de cierre y la ruta de guardado. Por otro lado, no concluya un corte de energía solo a partir de 41 o 6008. Indican una terminación inesperada, y una pantalla azul o un restablecimiento forzado también son candidatos.15

Elegir qué investigar a partir del registro de eventos de apagadoConfirmar el proceso iniciador y el motivo a partir de 1074, y tratar 41 y 6008 como pistas de una terminación inesperada para cruzar con información circundante como el código de comprobación de errores y los volcadosRegistro de eventos Sistema1074: proceso iniciador y motivoInvestigar el tratamiento de notificación y la ruta de guardado41 y 6008: terminación inesperadaComprobar BugcheckCode y volcadosAislar bloqueo, corte de energía, etc.

Figura 19: 41 y 6008 no son la prueba de un corte de energía en sí; son el punto de entrada a una investigación adicional.

Un BugcheckCode distinto de cero en el evento 41 es una pista de un bloqueo. Si es 0 y no hay volcado de memoria, se sospecha un corte de energía, pero decida cruzándolo con la información circundante. Si resulta ser un corte de energía, concéntrese en el diseño de guardado y el SAI del capítulo 8; si fue un cierre planificado, en las notificaciones y el cierre de los capítulos 3 a 6.

10. Resumen — diseñar hasta el siguiente arranque, no solo el tratamiento de cierre

El tratamiento del apagado no es la historia de un controlador de eventos que se ejecuta una vez al terminar. Lo que importa es hacer de el guardado habitual → un cierre breve → la validación y la recuperación en el siguiente arranque un todo continuo.

Dónde revisar Punto de diseño
Procesamiento habitual Guardar con frecuencia y reducir el delta restante al terminar
Notificaciones de cierre de la GUI Responder a la consulta con TRUE de inmediato, por norma. Separar el guardado idempotente del cierre posterior al compromiso
Consola y servicios Usar la notificación que encaja con el modelo de aplicación. No depender solo de ProcessExit de .NET ni de ampliar el margen
Trabajo ininterrumpible Combinar el registro del motivo y el rechazo solo el tiempo necesario. Prepararse también para un apagado forzado
Guardado y carga Además del archivo temporal, el vaciado y la sustitución, proporcionar una copia de seguridad y una validación al arrancar
Tras un reinicio Comprobar el registro de reinicio, los datos de restauración y la ruta de inicio de sesión o de arranque del servicio

En un apagado con inicio rápido activado, el kernel y los controladores pueden volver de la hibernación. Al aislar un problema, indique «reiniciar» de forma explícita, y haga que la aplicación funcione tanto en un apagado completo como en uno híbrido.5

Comprobación final del tratamiento de cierre a la recuperaciónMantener un estado guardado durante el procesamiento normal, hacer un cierre pequeño ante una notificación de cierre, y validar los datos que quedan incluso sin notificación para recuperar en el siguiente arranqueSíNoMantener un estado guardado de forma habitual¿Hay notificación de cierre?Terminar con un cierre breveEl último estado guardado es todo lo que hayValidar y recuperar en el siguiente arranqueVolver al trabajo en las condiciones requeridas

Figura 20: No separe la implementación del evento de cierre del guardado habitual y de la recuperación en el siguiente arranque.

Por último, pruebe las rutas de notificación y el tiempo requerido en una máquina de prueba, y confirme también la recuperación tras una parada súbita. En la investigación a posteriori, use 1074 / 41 / 6008 como pistas para aislar un cierre planificado de una terminación inesperada.

La próxima vez que añada una función, pregunte «si durante este procesamiento llega una notificación de cierre, o se arranca el cable, ¿qué quedará en el siguiente arranque?» Incluir esa respuesta en el diseño es lo que protege de descubrir la corrupción de datos recién a la mañana siguiente.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa del diseño e implementación de contramedidas de apagado y corte de energía para PC industriales y aplicaciones de larga duración, de la investigación de causas de corrupción de datos y fallos de «por la mañana estaba parado» que parten de un reinicio de Windows Update o de un cierre de sesión, y de las revisiones de diseño del procesamiento de detención de servicios de Windows y de la reanudación automática. Puede empezar desde la etapa de «parece que algo se rompe cada vez que apagamos, pero no sé por dónde empezar».

Referencias

  1. Microsoft Learn, WM_QUERYENDSESSION message. Sobre el envío de WM_QUERYENDSESSION al terminar una sesión y el hecho de que las aplicaciones deben devolver TRUE para respetar la intención del usuario (DefWindowProc también devuelve TRUE de forma predeterminada); sobre posponer el cierre hasta WM_ENDSESSION; sobre que el sistema muestra, a los 5 segundos, la interfaz de las aplicaciones que impiden el apagado para que el usuario pueda forzar la terminación; sobre el significado de los bits ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL de lParam; sobre que el apagado y el reinicio no se distinguen; y sobre guardar datos con frecuencia para reducir la cantidad que se guarda al terminar. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Microsoft Learn, HandlerRoutine callback function. Sobre los eventos CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN que recibe un controlador registrado con SetConsoleCtrlHandler; sobre que el tiempo de espera predeterminado de CTRL_CLOSE_EVENT es de unos 5000 milisegundos y el de CTRL_SHUTDOWN_EVENT para procesos de servicio de unos 20000 milisegundos; sobre que CTRL_LOGOFF/SHUTDOWN_EVENT lo reciben en la práctica solo los servicios porque las aplicaciones interactivas se terminan al cerrar sesión; y sobre que el controlador se ejecuta en un hilo distinto. ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, Service Control Handler Function. Sobre que los servicios que declaran SERVICE_ACCEPT_PRESHUTDOWN reciben SERVICE_CONTROL_PRESHUTDOWN primero, seguidos de los servicios SERVICE_ACCEPT_SHUTDOWN que reciben SERVICE_CONTROL_SHUTDOWN; sobre que el margen predeterminado en el apagado es de unos 20 segundos, con WaitToKillServiceTimeout como techo en un reinicio del sistema operativo; sobre no ampliar ese valor; sobre que el controlador de control vuelve en 30 segundos, informa STOP_PENDING con una indicación de espera y entrega el trabajo largo a otro hilo; sobre terminar el cierre lo más rápido posible teniendo en cuenta el funcionamiento con SAI; y sobre que el SCM no considera las dependencias en el apagado de forma predeterminada. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  4. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. Sobre que el runtime, desde .NET 10, ya no ofrece controladores predeterminados para Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (los equivalentes de SIGTERM/SIGHUP en Unix); sobre que el tratamiento predeterminado del sistema operativo termina la aplicación de inmediato de modo que AppDomain.ProcessExit y AssemblyLoadContext.Unloading ya no se disparan; y sobre que el tratamiento de señales adecuado al modelo de aplicación debe registrarlo una biblioteca de nivel superior o el código de la aplicación. ↩ ↩2 ↩3

  5. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. Sobre que la sesión del kernel no se cierra sino que se hiberna con el inicio rápido, y el estado del kernel y de los controladores de dispositivo se guarda en hiberfil.sys; sobre que «Reiniciar» siempre realiza un arranque completo porque se requiere un estado de Windows completamente nuevo; sobre que el inicio rápido está activado de forma predeterminada y desactivarlo no se recomienda; y sobre que Shutdown.exe tiene como valor predeterminado un apagado completo y la opción /hybrid da el comportamiento híbrido. ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Shutdown Changes for Windows Vista. Sobre que las respuestas a WM_QUERYENDSESSION/WM_ENDSESSION se pueden retrasar 5 segundos cada una, tras lo cual el usuario elige continuar o cancelar; sobre que las aplicaciones de consola y las que no tienen ventana visible no pueden cancelar el apagado y se terminan automáticamente a los 5 segundos sin respuesta o ante una respuesta FALSE; sobre registrar un motivo con ShutdownBlockReasonCreate cuando hace falta bloquear; y sobre que las aplicaciones no deben depender de poder bloquear el apagado. ↩ ↩2 ↩3

  7. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). Sobre llamarla al inicio de un trabajo ininterrumpible para registrar una cadena de motivo y llamar a ShutdownBlockReasonDestroy al completar; sobre que solo se puede llamar desde el hilo que creó la ventana; y sobre mantener la cadena corta y clara porque el usuario solo lee el motivo unos segundos. ↩ ↩2 ↩3

  8. Microsoft Learn, SetConsoleCtrlHandler function. Sobre que un proceso que ha cargado gdi32.dll o user32.dll se trata como una aplicación de Windows cuyos controladores de CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT no se llaman; sobre la alternativa de crear una ventana oculta y tratar WM_QUERYENDSESSION/WM_ENDSESSION; y sobre que las funciones de consola pueden no funcionar con normalidad durante el tratamiento de señales. ↩

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). Sobre que el SCM espera tras la notificación PRESHUTDOWN a que el servicio se detenga o expire el tiempo; sobre que el tiempo de espera predeterminado es de 10 segundos desde Windows 10 Creators Update (compilación 15063) en adelante y de 3 minutos antes; sobre configurarlo con ChangeServiceConfig2; y sobre que las actualizaciones de estado pueden continuar durante SERVICE_STOP_PENDING. ↩ ↩2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). Sobre registrarse para el reinicio en los escenarios de bloqueo, de no responder, de actualización y de reinicio del equipo impulsado por una actualización; sobre indicar argumentos de línea de comandos para el reinicio; sobre registrarse antes de que ocurra un problema, siendo el tratamiento de WM_QUERYENDSESSION la última oportunidad en el escenario de actualización; sobre que los procesos con menos de 60 segundos de ejecución no se reinician; sobre que el reinicio tras un bloqueo o un cuelgue exige el consentimiento del usuario; y sobre que atravesar un reinicio del sistema operativo exige un apagado con EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Sobre que Windows Update guarda las credenciales del último usuario interactivo y configura Autologon cuando inicia un reinicio automático; sobre que el usuario inicia sesión automáticamente tras el reinicio y la sesión se bloquea; sobre que las credenciales guardadas se eliminan tras un inicio de sesión correcto; y sobre la configuración mediante Directiva de grupo (DisableAutomaticRestartSignOn y otras). ↩ ↩2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). Sobre que ReplaceFile combina en una función los varios pasos que corresponden a guardar en un archivo nuevo, cambiar temporalmente el nombre del original, cambiar el nombre del archivo nuevo y eliminar el original; sobre que conserva atributos del archivo original como la fecha de creación, la DACL, el cifrado, la compresión y los flujos con nombre; y sobre que la copia de seguridad, el archivo sustituido y el de sustitución deben estar en el mismo volumen. ↩ ↩2

  13. Microsoft Learn, File Caching. Sobre que las escrituras van de forma predeterminada a la caché del sistema y se aplican al disco con escritura diferida; sobre que FILE_FLAG_WRITE_THROUGH escribe los datos en el disco de inmediato; sobre que FlushFileBuffers vacía de forma explícita; y sobre que los metadatos del sistema de archivos siempre se almacenan en caché, de modo que confirmar metadatos exige un vaciado o write-through. ↩ ↩2

  14. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. Sobre que este evento se entrega mediante WM_POWERBROADCAST al cambiar entre batería y alimentación de CA o cuando baja la carga restante; y sobre llamar a GetSystemPowerStatus al recibirlo para comprobar ACLineStatus, BatteryFlag, BatteryLifePercent y otros miembros de SYSTEM_POWER_STATUS. ↩ ↩2

  15. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. Sobre que un reinicio normal registra el identificador de evento 1074 (qué proceso inició el apagado, en nombre de quién y por qué motivo); sobre que un reinicio inesperado registra el identificador de evento 41 (Kernel-Power) y 6008 (el apagado anterior no se esperaba); y sobre usar estos identificadores para aislar el tipo de reinicio. ↩ ↩2

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.

Un problema que no se solucionó con «Apagar» sí se solucionó al «Reiniciar». ¿Por qué?
En los sistemas operativos cliente a partir de Windows 8, cuando el inicio rápido está activado (el valor predeterminado en muchos PC que admiten hibernación), «Apagar» funciona mediante un mecanismo llamado apagado híbrido. Se cierra la sesión del usuario, pero el estado del kernel y de los controladores se guarda en el archivo de hibernación y se restaura tal cual en el siguiente arranque. Es decir, el núcleo del sistema operativo no se ha restablecido. En cambio, «Reiniciar» siempre realiza un arranque completo, de modo que los problemas de controladores y servicios sí se restablecen. En el procedimiento de aislamiento, indique «reiniciar» de forma explícita, no «apagar y volver a encender». Si desea un apagado completo desde la línea de comandos, shutdown /s lo hace.
¿Se puede detener el apagado hasta que la aplicación termine de guardar?
Se puede pedir una espera temporal, pero no se puede detener con fiabilidad. Si registra una cadena de motivo con ShutdownBlockReasonCreate solo mientras dura una operación que no se puede interrumpir, ese motivo aparece en la pantalla «Esta aplicación está impidiendo el apagado» y el usuario puede decidir si continúa o cancela. El usuario, aun así, puede elegir forzar la continuación, y un apagado forzado o un reinicio por actualización puede no esperar en absoluto. El enfoque correcto, por tanto, no es bloquear, sino reducir los datos en riesgo con un autoguardado frecuente y diseñar un cierre que termine en unos segundos a partir de la notificación de fin.
La detención de mi servicio de Windows tarda mucho. ¿Se puede ampliar el margen de gracia en el apagado?
En la configuración predeterminada, que recibe SERVICE_CONTROL_SHUTDOWN, el margen es de unos 20 segundos y depende del valor de Registro WaitToKillServiceTimeout. No se recomienda que la aplicación reescriba ese valor para ampliarlo. Si necesita un margen más largo, puede declarar SERVICE_ACCEPT_PRESHUTDOWN y recibir SERVICE_CONTROL_PRESHUTDOWN; se entrega antes que los demás, y el tiempo de espera se configura con ChangeServiceConfig2 (el valor predeterminado es de 10 segundos desde Windows 10 Creators Update en adelante, y de 3 minutos antes). PRESHUTDOWN, no obstante, retiene todo el apagado durante ese intervalo, así que limítelo a los casos que realmente lo necesitan y, en el fondo, diseñe el propio procesamiento de detención para que termine en unos segundos.
¿Es seguro hacer el cierre en el apagado con AppDomain.ProcessExit de .NET?
Se recomienda no depender de él. Antes, el runtime registraba controladores de señal predeterminados y el evento ProcessExit se disparaba con CTRL_CLOSE_EVENT y CTRL_SHUTDOWN_EVENT, pero desde .NET 10 el runtime ya no ofrece controladores de señal de terminación predeterminados, y ProcessExit ya no se dispara en esas situaciones. Implemente el cierre en la ruta de notificación que corresponda al modelo de aplicación: en aplicaciones GUI, FormClosing o SessionEnding (son notificaciones de la fase de consulta, así que limítelas a un guardado idempotente y haga el cierre que solo es posible tras el compromiso del fin en un gancho de WM_ENDSESSION); en Generic Host / Worker Service, IHostApplicationLifetime y StopAsync; en aplicaciones de consola, SetConsoleCtrlHandler o PosixSignalRegistration.
¿Cómo se evita que los archivos se dañen por un corte de energía repentino?
Un corte de energía no trae ninguna notificación, así que la única opción es escribir de forma que no se rompa sin importar cuándo se corte la alimentación. Lo básico es no sobrescribir el archivo original en el sitio: escribirlo todo en un archivo temporal del mismo volumen, vaciar el búfer y sustituir con ReplaceFile (File.Replace en .NET). En el funcionamiento normal esto deja legible el archivo antiguo completo o el nuevo completo, pero la atomicidad de ReplaceFile a través de un corte de energía no está garantizada por la especificación, así que conserve una copia de seguridad (el tercer argumento) e implemente, como un conjunto, una rutina de carga que valide el archivo principal al arrancar y recurra a la copia de seguridad si está dañado. Además, el éxito de WriteFile no significa que los datos hayan llegado al disco, así que en los hitos importantes confirme la escritura con FlushFileBuffers o FILE_FLAG_WRITE_THROUGH. En los PC industriales, la configuración habitual añade un SAI, detecta el paso a batería y encadena con un apagado seguro.

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