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: · Go Komura · 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.
flowchart TB
accTitle: Un diseño de guardado común a la notificación de cierre y al corte de energía
accDescr: El 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 trabajo
daily["Guardar en cada hito"] --> event{"¿Hay notificación al terminar?"}
event -->|"Sí"| close["Guardar solo el delta restante y terminar"]
event -->|"No"| lost["No es posible un cierre en el momento"]
close --> boot["Validar y recuperar en el siguiente arranque"]
lost --> boot
boot --> resume["Reanudar 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.
flowchart TB
accTitle: Pensar el cierre de sesión y la salida del sistema por separado
accDescr: El 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 ninguno
signout["Cierre de sesión"] --> user["Las aplicaciones del usuario terminan"]
signout -.-> alive["Los servicios siguen en marcha"]
system["Apagado o reinicio"] --> user
system --> svc["Los servicios también se detienen"]
power["Corte de energía"] --> none["Sin 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
flowchart TB
accTitle: Inicio rápido frente a un arranque completo
accDescr: Un 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 completo
s["Apagado"] --> q{"¿Usar el inicio rápido?"}
q -->|"Sí"| save["Hibernar el kernel y los controladores"]
save --> restore["Restaurar el estado en el siguiente arranque"]
q -->|"No"| full["Apagado completo"]
full --> boot["Inicializar con un arranque completo la próxima vez"]
r["Reinicio"] --> boot
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
flowchart TB
accTitle: La consulta de la GUI y el resultado del cierre
accDescr: Incluso 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 TRUE
query["WM_QUERYENDSESSION"] --> reply["Devolver TRUE de inmediato, por norma"]
reply --> result["WM_ENDSESSION"]
result --> yes{"¿wParam es TRUE?"}
yes -->|"Sí"| cleanup["Cierre posterior al compromiso"]
yes -->|"No"| running["Cierre 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
flowchart TB
accTitle: Mantener pequeño el trabajo al terminar
accDescr: Un 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 forzada
good["Guardar de forma habitual"] --> small["El delta restante al terminar es pequeño"]
small --> fast["Terminar con un cierre breve"]
bad["Acumular en memoria hasta el cierre"] --> large["Guardarlo todo al terminar"]
large --> risk["Poco 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.
flowchart TB
accTitle: Reparto del tratamiento de cierre en WinForms y WPF
accDescr: FormClosing 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 deshacer
notify["Fase de consulta"] --> forms["FormClosing"]
notify --> wpf["SessionEnding"]
forms --> snapshot["Guardado de instantánea idempotente"]
wpf --> snapshot
final["WM_ENDSESSION con TRUE"] --> hook["Recibir en WndProc o un gancho"]
hook --> cleanup["Trabajo 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.
flowchart TB
accTitle: Reparto de roles en un bloqueo temporal de cierre
accDescr: Solo 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 evitarse
start["Empieza el trabajo ininterrumpible"] --> reason["Registrar el motivo y activar el indicador de protección"]
reason --> work["Procesar en un trabajador"]
work --> done["Terminar; quitar el motivo y el indicador"]
work -.-> request["Llega una consulta de cierre entretanto"]
request --> refuse["Devolver FALSE y mostrar el motivo"]
refuse --> choice{"Decisión del usuario"}
choice -->|"Cancelar"| keep["Seguir en marcha"]
choice -->|"Forzar"| terminate["Puede 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
flowchart TB
accTitle: Condiciones para recibir las notificaciones de cierre de la consola
accDescr: Una 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 GUI
console["Proceso de consola"] --> close["El cierre va al controlador de control"]
console --> q{"¿Esperar LOGOFF o SHUTDOWN?"}
q -->|"Sesión interactiva"| no["No se puede confiar en esta notificación"]
q -->|"Servicio"| dll{"¿Se cargaron las DLL de GUI?"}
dll -->|"No"| signal["Tratar la señal de control correspondiente"]
dll -->|"Sí"| window["Recibir 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.
flowchart TB
accTitle: Quitar la dependencia del controlador de terminación predeterminado de .NET
accDescr: El 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ón
signal["Señal CLOSE o SHUTDOWN"] --> old["Tratamiento predeterminado del runtime anterior"]
old --> event["Dispara ProcessExit y similares"]
signal --> modern["Sin tratamiento predeterminado desde .NET 10"]
modern --> own{"¿La aplicación lo trata?"}
own -->|"No"| os["Terminado por el tratamiento predeterminado del SO"]
own -->|"Sí"| handle["Cierre 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.
flowchart TB
accTitle: El procesamiento de detención del Host y el margen de cierre exterior
accDescr: La 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 ambos
outer["Petición de cierre del SO o del SCM"] --> host["Ruta de detención del Host"]
host --> stop["Cerrar en StopAsync"]
stop --> inner["Fijar el tiempo de detención del Host"]
outer -.-> limit["El margen exterior existe por separado"]
inner --> check["Medir si termina en poco tiempo"]
limit --> check
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 |
flowchart TB
accTitle: Etapas de la notificación de cierre a los servicios
accDescr: El SCM notifica primero a los servicios que aceptan PRESHUTDOWN, espera a que se detengan o expiren y luego sigue hacia la notificación SHUTDOWN ordinaria
start["Empieza el apagado del sistema"] --> pre["Notificar a los servicios que aceptan PRESHUTDOWN"]
pre --> wait["Esperar la detención o el plazo configurado"]
wait --> shut["Notificar a los servicios que aceptan SHUTDOWN"]
shut --> proceed["Tras 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
flowchart TB
accTitle: Reparto del trabajo entre el controlador de control del servicio y el trabajador
accDescr: El 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 detenido
handler["Controlador de control"] --> pending["Informar STOP_PENDING"]
pending --> signal["Señalar al trabajador que se detenga"]
signal --> back["El controlador vuelve de inmediato"]
signal --> worker["El trabajador cierra"]
worker --> report["Informar el progreso si tarda"]
report --> stopped["Informar 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.
flowchart TB
accTitle: Guardado y notificación de progreso en la devolución de llamada de recuperación
accDescr: La 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 completar
reg["Preregistrar la devolución de llamada de recuperación"] --> crash["Bloqueo; WER la invoca"]
crash --> save["Guardar los datos en los que se trabaja"]
save --> progress["Informar el progreso dentro del intervalo de ping"]
progress --> completed{"¿Guardado completo?"}
completed -->|"Aún no"| save
completed -->|"Hecho"| done["Notificar la recuperación terminada"]
progress -.-> timeout["Puede 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
flowchart TB
accTitle: Registro de reinicio, inicio de sesión y restauración de datos
accDescr: Frente 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 restaurado
reg["Registrarse para el reinicio antes de que ocurra un problema"] --> crash["Bloqueo o no responde"]
crash --> consent["Obtener el consentimiento del usuario"]
consent --> app["Reiniciar la aplicación"]
reg --> update["Reinicio del SO con los indicadores requeridos"]
update --> session["Restaurar la sesión mediante ARSO o similar"]
session --> app
app --> data["Leer el punto de restauración guardado"]
session -.-> policy["Comprobar 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
flowchart TB
accTitle: Guardar un archivo y recuperar en el siguiente arranque
accDescr: Escribir 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 falta
tmp["Archivo temporal en el mismo volumen"] --> write["Escribir hasta el final y vaciar"]
write --> replace["Sustituir; el contenido antiguo va a .bak"]
replace -.-> boot["Siguiente arranque"]
boot --> valid{"¿El archivo principal está intacto?"}
valid -->|"Sí"| main["Leer el archivo principal"]
valid -->|"No"| backup["Recurrir 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
flowchart TB
accTitle: El límite entre el éxito de escritura y la persistencia
accDescr: Una 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 dispositivo
write["WriteFile tiene éxito"] --> cache["Puede estar en la caché del SO"]
cache --> delayed["Escritura diferida"]
cache --> flush["Vaciar en un hito"]
delayed --> device["Aplicado al almacenamiento"]
flush --> device
device -.-> limit["Lí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
flowchart TB
accTitle: De la detección del SAI a la finalización del apagado
accDescr: Detectar 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ía
outage["Apagón; el SAI pasa a batería"] --> notice["Notificación de energía en la ruta correspondiente"]
notice --> check["Comprobar el estado de energía y la carga restante"]
check --> save["Suspender la medición y guardar"]
save --> req["Pedir un apagado al sistema operativo"]
req --> done["Completar el cierre y la salida del SO"]
done -.-> time["Hacer 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
flowchart TB
accTitle: Ampliar la verificación del apagado por etapas
accDescr: Probar 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ón
prep["Preparar la máquina de prueba y los datos"] --> notify["Probar las rutas de notificación y las operaciones de cierre"]
notify --> time["Medir el tiempo de cierre"]
time --> vm["Confirmar la recuperación tras una parada súbita de la MV"]
vm --> real["Verificar en hardware real incluyendo el almacenamiento"]
real --> check["Confirmar 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
flowchart TB
accTitle: Elegir qué investigar a partir del registro de eventos de apagado
accDescr: Confirmar 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 volcados
log["Registro de eventos Sistema"] --> normal["1074: proceso iniciador y motivo"]
normal --> cleanup["Investigar el tratamiento de notificación y la ruta de guardado"]
log --> unexpected["41 y 6008: terminación inesperada"]
unexpected --> evidence["Comprobar BugcheckCode y volcados"]
evidence --> classify["Aislar 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
flowchart TB
accTitle: Comprobación final del tratamiento de cierre a la recuperación
accDescr: Mantener 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 arranque
daily["Mantener un estado guardado de forma habitual"] --> endq{"¿Hay notificación de cierre?"}
endq -->|"Sí"| short["Terminar con un cierre breve"]
endq -->|"No"| prior["El último estado guardado es todo lo que hay"]
short --> nextboot["Validar y recuperar en el siguiente arranque"]
prior --> nextboot
nextboot --> restart["Volver 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
- Cómo reemplazar un exe o una DLL en uso — Restart Manager y el problema del «archivo en uso» en las actualizaciones automáticas
- 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
- Suspensión, hibernación y Modern Standby: evitar con diseño que las apps de larga duración se detengan de noche
- Las profundidades de la E/S de Windows (parte 4) — El administrador de caché: ¿cuándo llega su WriteFile al disco?
- Diseño para conservar registros y volcados de memoria cuando falla una aplicación de Windows
- Lista de verificación para manejar procesos secundarios de forma segura en aplicaciones de Windows
Á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».
- Desarrollo de aplicaciones Windows
- Investigación de fallos y análisis de causas
- Consultoría técnica y revisión de diseño
- Contacto
Referencias
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
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. ↩
-
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
-
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
-
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
-
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
-
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
-
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
-
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 relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Lo que hace realmente el inicio rápido — por qué «Apagar» en Windows no es lo mismo que reiniciar
Un apagado de Windows es por defecto un apagado híbrido que guarda el kernel y los controladores en hiberfil.sys. Por qué solo un reinici...
La red funciona pero Windows dice «Sin conexión a Internet» — Acotar NCSI, DNS, proxy y VPN en Windows
Por qué Windows dice «Sin conexión a Internet» mientras la red funciona, partiendo del veredicto de NCSI. Acotar DNS, proxy, VPN y portal...
Qué queda después de que muere el padre — mantener los procesos hijos en un Job Object
Por qué los ayudantes del SDK sobreviven a una IU terminada y retienen la cámara o el puerto COM. Cómo diseñar la vida de los procesos hi...
¿Qué es un objeto OLE? — Incrustación, vinculación y las trampas de los documentos empresariales
La función que incrusta una tabla de Excel en Word es, en realidad, un objeto OLE. El artículo explica en clave práctica la diferencia en...
API del grupo de hilos de Win32 — Concurrencia sin crear hilos, con CreateThreadpoolWork
¿Está multiplicando las llamadas a CreateThread en el código nativo? Este artículo explica, a partir de fuentes primarias, la API del gru...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
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.