El apagado de Windows visto desde la aplicación ── cómo sobrevivir correctamente a la notificación de cierre, el reinicio y el corte de energía
· Actualizado el: · Go Komura · Windows, Apagado, Desarrollo en Windows, Servicios de Windows, PC industrial, Protección de datos, Funcionamiento prolongado, SAI
«Después de un reinicio nocturno por Windows Update, la aplicación de medición del PC industrial se cayó junto con los datos que estaba escribiendo, y por la mañana el archivo de medición estaba dañado» — «Al cerrar sesión en un PC compartido, llegó una queja de que el contenido que se estaba editando desapareció sin guardarse» ── estos dos casos son clásicos en las consultas sobre aplicaciones de Windows que funcionan de forma prolongada.
Lo que tienen en común ambos casos es tratar el apagado como «un imprevisto que no debería ocurrir». Pero en realidad, desde el reinicio automático de Windows Update, el cierre de sesión del usuario o la orden de apagado de un SAI hasta el corte de energía sin previo aviso, los eventos que interrumpen la ejecución desde fuera de la aplicación siempre llegan, tarde o temprano. El hecho de que lleguen no se puede evitar. Lo que sí se puede evitar es «perder datos cuando lleguen».
Por suerte, Windows dispone de un mecanismo que notifica a la aplicación antes del apagado, tanto para apps GUI como para apps de consola y servicios. Este artículo está 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 funcionamiento prolongado), y organiza, con base en las fuentes oficiales de Microsoft Learn a fecha de agosto de 2026, cómo recibir la notificación, cómo diseñar un cierre que «se complete en segundos», la recuperación automática tras el reinicio y cómo prepararse ante un corte de energía sin notificación.
1. Conclusión principal
- Diseñe el apagado como «un evento normal que siempre llega, tarde o temprano». El tiempo disponible tras recibir la notificación es, en principio, de solo unos 5 segundos, así que un diseño que intente guardarlo todo apresuradamente en ese momento está condenado a fallar. La base es reducir de antemano, mediante un autoguardado frecuente, «lo que hay que guardar en el momento del apagado».1
- En los sistemas operativos cliente desde Windows 8, cuando el inicio rápido está activado (el valor predeterminado en muchos PC que admiten hibernación), «Apagar» es un apagado híbrido, y el núcleo solo está hibernando. Lo único que se reinicia por completo es «Reiniciar». Ese es el verdadero motivo de «lo apagué pero no se arregló, y al reiniciar sí se arregló».2
- Las apps GUI deben devolver TRUE de inmediato a WM_QUERYENDSESSION, y el cierre se hace en WM_ENDSESSION. Por norma general, no se debe devolver FALSE (rechazo).1
- Solo cuando de verdad haya un proceso que no se pueda interrumpir, muestre el motivo con ShutdownBlockReasonCreate. Aun así, tanto el usuario como el sistema operativo pueden forzar la continuación, así que un diseño que dé por hecho que «se puede bloquear» no es viable.34
- Las apps de consola reciben la notificación con SetConsoleCtrlHandler. El margen es aún más corto, 5 segundos por defecto para el cierre de consola. Hay una trampa: en los procesos que cargan gdi32.dll o user32.dll, algunos de estos eventos no llegan.56
- En .NET, el cierre que depende de AppDomain.ProcessExit deja de funcionar, desde .NET 10, en las rutas de «terminación desde fuera». En la terminación normal, como el retorno desde Main, sigue funcionando como antes, pero como el runtime ya no ofrece un procesamiento predeterminado para señales de terminación como el cierre de consola o el apagado, el cierre de esa ruta debe trasladarse a la notificación propia de cada modelo de aplicación.7
- En los servicios de Windows, SERVICE_ACCEPT_PRESHUTDOWN recibe la notificación antes que SERVICE_ACCEPT_SHUTDOWN (margen de unos 20 segundos) y con un margen configurable. Sin embargo, el tiempo de espera predeterminado de PRESHUTDOWN se redujo a 10 segundos desde Windows 10 Creators Update, así que en cualquier caso hace falta un diseño que no dependa en exceso del margen.89
- La recuperación automática tras el reinicio se puede lograr combinando RegisterApplicationRestart con ARSO (inicio de sesión automático). Hay una ruta de recuperación preparada tanto para el fallo, como para la falta de respuesta, como para el reinicio por actualización.1011
- Un corte de energía no trae ninguna notificación. Lo habitual es escribir por completo en un archivo temporal, vaciarlo y sustituirlo con ReplaceFile, pero como ReplaceFile tampoco garantiza la atomicidad ante un corte de energía, hace falta además una ruta de recuperación con copia de seguridad (.bak) y verificación en el arranque. El diagnóstico posterior se puede hacer con el registro de eventos (1074/41/6008).121314
En una sola frase, la conclusión de este artículo es: «mantener siempre un estado en el que, al llegar la notificación, se pueda cerrar en segundos, y escribir de forma que tampoco se rompa ante un corte de energía sin notificación».
2. Qué ocurre durante el apagado ── los cuatro «finales»
2.1. Cierre de sesión, apagado, reinicio y corte de energía
Desde el punto de vista de la aplicación, lo importante son dos ejes: «cómo termina la sesión de usuario» y «qué le pasa al núcleo».
| Operación | Sesión de usuario | Núcleo y controladores | Notificación a la aplicación |
|---|---|---|---|
| Cierre de sesión | Termina | Sigue funcionando | WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION |
| Apagado (con inicio rápido activado) | Termina | Hiberna (se guarda en hiberfil.sys) | WM_QUERYENDSESSION → WM_ENDSESSION, (PRE)SHUTDOWN a los servicios |
| Reinicio | Termina | Termina por completo y hace un arranque completo la próxima vez | Igual que arriba |
| Corte de energía | Desaparece de inmediato | Desaparece de inmediato | Ninguna |
El cierre de sesión y el apagado son, vistos desde la aplicación, prácticamente el mismo evento. Si el bit ENDSESSION_LOGOFF está activo en el lParam de WM_QUERYENDSESSION es un cierre de sesión; si es 0, es un apagado o un reinicio (no se pueden distinguir entre sí).1 Es decir, la confianza de «con que sea un simple cierre de sesión no pasa nada» no es válida, y lo correcto es que se llame el mismo código de cierre en ambos casos.
flowchart TB
accTitle: Los cuatro "finales" y la notificación a la aplicación
accDescr: En el cierre de sesión, el apagado y el reinicio llega la notificación de WM_QUERYENDSESSION a WM_ENDSESSION y el cierre se hace en segundos. Solo el corte de energía no tiene ninguna notificación y solo se puede afrontar con el diseño de escritura
signout["Cierre de sesión"] --> notified["Con notificación: WM_QUERYENDSESSION → WM_ENDSESSION (a los servicios: (PRE)SHUTDOWN)"]
shutdown["Apagado"] --> notified
reboot["Reinicio"] --> notified
poweroff["Corte de energía"] --> none["Sin notificación ── se afronta con el diseño de escritura y el SAI del capítulo 8"]
notified --> cleanup["Cierre en segundos (capítulos 3 a 6)"]
2.2. La verdadera causa de «lo apagué pero no se arregló» ── el apagado híbrido
La segunda fila de la tabla es fácil de pasar por alto. Desde Windows 8, en los sistemas operativos cliente, el inicio rápido (apagado híbrido) viene activado por defecto en los PC que admiten hibernación, y el comportamiento de «Apagar» cambió. El cierre de sesión del usuario se sigue haciendo con normalidad, pero la sesión del núcleo no se cierra: se guarda, junto con los controladores de dispositivo, en el archivo de hibernación (hiberfil.sys) y se restaura tal cual en el siguiente arranque. Esto hace que el arranque sea más rápido, pero el estado del núcleo y de los controladores se conserva aunque se corte la energía.2 Ahora bien, este es un comportamiento condicional. En entornos donde la propia hibernación está desactivada (powercfg /hibernate off), donde el inicio rápido está desactivado por directiva o por opciones de energía, y en Windows Server, el apagado vuelve a ser el apagado completo tradicional. Para saber en cuál de los dos modos funciona un PC concreto, puede comprobar la casilla «Activar inicio rápido» de las opciones de energía, o usar powercfg /a (que indica si aparece «Inicio rápido» entre los estados de suspensión disponibles).
flowchart TB
accTitle: Qué le ocurre al núcleo con la operación de apagado
accDescr: La operación de apagado se divide en apagado completo o hibernación del núcleo según el inicio rápido esté activado o no, mientras que el reinicio siempre hace un arranque completo
op["Operación «Apagar»"]
restart["Operación «Reiniciar»"]
op -->|"Inicio rápido activado (predeterminado en cliente)"| hybrid["Fin de la sesión de usuario + el núcleo hiberna en hiberfil.sys"]
op -->|"Hibernación desactivada · directiva la desactiva · Windows Server"| full["Apagado completo"]
hybrid --> resume["Próximo arranque: se restaura el estado del núcleo y los controladores"]
full --> boot["Próximo arranque: se inicializa con arranque completo"]
restart --> boot
Por otro lado, «Reiniciar» siempre ejecuta un ciclo de arranque completo, porque a veces hace falta un estado totalmente nuevo, por ejemplo tras actualizar controladores.2 A partir de esto se explica con claridad un fenómeno muy habitual en la práctica.
- «Apagué el equipo y volví a encenderlo, pero el problema del dispositivo no se arregló» ── el núcleo y los controladores solo se restauraron desde la hibernación, no se reiniciaron
- «Al reiniciar sí se arregló» ── porque se inicializó con un arranque completo
- En los procedimientos de respuesta a fallos de un PC industrial, se debe escribir «reiniciar» y no «apagar y volver a encender»
Si quiere indicar explícitamente un apagado completo con un comando, puede usar shutdown /s (el comportamiento predeterminado de Shutdown.exe es el apagado completo), y si quiere reproducir el comportamiento híbrido predeterminado, shutdown /s /hybrid.2 Cabe señalar que no se recomienda desactivar el inicio rápido. La aplicación debe partir de la premisa de que «en el apagado puede que el núcleo solo esté hibernando»; por ejemplo, no debe estimar el «tiempo de funcionamiento acumulado» a partir de la hora de arranque del sistema operativo (dado que el inicio rápido puede estar activado o no según el entorno, el diseño debe funcionar correctamente en ambos casos).
3. Las buenas prácticas en apps GUI ── WM_QUERYENDSESSION y WM_ENDSESSION
3.1. El reparto de funciones entre los dos mensajes
En las aplicaciones que tienen una ventana y una cola de mensajes, el fin de la sesión se notifica en dos fases.1
- WM_QUERYENDSESSION ── una consulta de «¿se puede terminar?». La aplicación debe devolver TRUE de inmediato, y la respuesta predeterminada de DefWindowProc también es TRUE. No se debe empezar el cierre aquí.
- WM_ENDSESSION (wParam=TRUE) ── la notificación de confirmación de que «la sesión realmente va a terminar». El cierre se hace aquí.
Devolver FALSE en WM_QUERYENDSESSION también puede cancelar el apagado, pero la documentación deja claro que «se debe respetar la intención del usuario y devolver TRUE», y una aplicación que devuelva FALSE también queda expuesta en la UI a pantalla completa como «aplicación que está impidiendo el apagado». Además, una aplicación de consola o sin ventana visible no puede cancelar el apagado de entrada, y si no responde en 5 segundos se termina automáticamente de forma forzada.14
flowchart TB
accTitle: Flujo de la notificación en dos fases del fin de sesión
accDescr: Si se responde TRUE a la consulta de WM_QUERYENDSESSION, WM_ENDSESSION lo confirma y ahí se hace el cierre. Si se rechaza con FALSE, la aplicación se muestra como la que está impidiendo el apagado, y si no responde en unos 5 segundos puede forzarse la continuación
q["WM_QUERYENDSESSION (consulta)"]
q -->|"TRUE (norma general)"| e["WM_ENDSESSION wParam=TRUE (confirmación)"]
q -->|"FALSE (rechazo excepcional)"| blocked["Se muestra en una UI a pantalla completa como «app que impide el apagado»"]
q -->|"sin respuesta en unos 5 s"| hung["Se trata como sin respuesta"]
e --> cleanup["Aquí se hace el cierre (guardar, desconectar)"]
cleanup --> term["Fin del proceso"]
hung --> term
blocked -->|"El usuario fuerza la continuación"| term
blocked -->|"El usuario cancela"| cont["Se cancela el apagado → continúan todas las apps"]
3.2. Qué pasa si no se responde ── el muro de los 5 segundos
Tanto en WM_QUERYENDSESSION como en WM_ENDSESSION, la respuesta solo se puede retrasar unos 5 segundos. Si se supera ese tiempo, el sistema muestra la pantalla «esta aplicación está impidiendo el apagado» y el usuario puede elegir forzar la continuación (es decir, terminar la aplicación de forma forzada).4 Un proceso terminado de forma forzada no tiene ninguna oportunidad de continuar con su proceso de guardado.
Por lo tanto, el diseño se reduce a estos dos puntos:
- Mantener el cierre dentro de una cantidad que termine en menos de 5 segundos. El propio Microsoft recomienda guardar los datos con frecuencia en el día a día para reducir la cantidad que hay que guardar en el momento del apagado, y guardar los datos sin guardar en una ubicación temporal para restaurarlos en el siguiente arranque.1
- No mostrar cuadros de diálogo de confirmación durante el apagado. Si se pregunta «¿desea guardar?» y se espera la respuesta, los 5 segundos se agotan mientras tanto. Hay que inclinarse en silencio hacia el lado seguro (el autoguardado).
3.3. Implementación en WinForms y WPF
En las aplicaciones de escritorio de .NET, estos mensajes se traducen a eventos del framework. En WinForms se llama a FormClosing, y con CloseReason se puede distinguir si el origen es el apagado.
// WinForms: FormClosing también se llama en el apagado o el cierre de sesión
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.WindowsShutDown)
{
// Solo se hace un guardado idempotente de instantánea. No se muestra ningún diálogo.
// Tampoco se establece e.Cancel = true (rechazo).
SaveWorkingStateToTempFile();
return;
}
// Cuando el usuario cierra con el botón x, entre otros casos normales, aquí sí se puede confirmar
}
En WPF, el equivalente es el evento Application.SessionEnding (el atributo SessionEnding de XAML, o la sobrecarga de OnSessionEnding).
flowchart LR
accTitle: Correspondencia entre eventos de WinForms/WPF y los mensajes
accDescr: La fase de consulta de WM_QUERYENDSESSION corresponde a FormClosing en WinForms y a SessionEnding en WPF, y ahí solo se debe hacer un guardado idempotente de instantánea. La notificación de confirmación WM_ENDSESSION no tiene evento equivalente, así que se recibe con WndProc o un hook para hacer el cierre posterior a la confirmación
q["WM_QUERYENDSESSION (consulta)"] --> fc["WinForms: FormClosing (WindowsShutDown)"]
q --> se["WPF: SessionEnding"]
fc -.-> idem["Solo guardado idempotente de instantánea"]
se -.-> idem
e["WM_ENDSESSION (confirmación)"] --> hook["Sin evento equivalente → se recibe con WndProc/hook"]
hook -.-> final["Cierre que solo puede hacerse tras la confirmación"]
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
base.OnSessionEnding(e);
// Se puede distinguir ReasonSessionEnding.Logoff / Shutdown,
// pero lo básico es ejecutar el mismo guardado de instantánea en ambos casos
SaveWorkingStateToTempFile();
// No se establece e.Cancel = true salvo que haya un motivo de peso
}
Aquí hay un aviso importante. Tanto FormClosing (CloseReason.WindowsShutDown) como SessionEnding de WPF corresponden a la fase de consulta (WM_QUERYENDSESSION). Si otra aplicación la rechaza, el apagado se cancela y la propia aplicación sigue funcionando con normalidad. Por lo tanto, lo único que se debe hacer en estos eventos es un guardado de instantánea idempotente, que no cause ningún daño aunque se cancele y que dé el mismo resultado sin importar cuántas veces se ejecute. Si hace falta un «cierre que solo se debe hacer cuando realmente termina» (como desconectar una conexión o liberar un recurso), hay que enganchar directamente en WndProc la notificación de confirmación WM_ENDSESSION (wParam=TRUE) y hacerlo ahí.
En cualquiera de las dos rutas conviene centralizar el contenido en una «función de guardado de instantánea» común, y escribir los datos de recuperación de la terminación normal, del apagado y (si es posible) de un fallo en el mismo formato, de modo que la lógica de restauración del siguiente arranque quede en un solo lugar. El diseño para conservar información incluso en caso de fallo se trata en «Cómo dejar registros y volcados al fallar una aplicación de Windows».
4. Si de verdad hay que bloquear ── ShutdownBlockReasonCreate
Solo son una excepción los procesos que se dañan físicamente si se interrumpen a medias, como grabar un CD o escribir firmware. En este caso, la práctica correcta es registrar un texto de motivo con ShutdownBlockReasonCreate al iniciar el proceso que no se puede interrumpir, y liberarlo de inmediato con ShutdownBlockReasonDestroy en cuanto termine. Cuando se solicita el apagado, este motivo se muestra en la pantalla «esta aplicación está impidiendo el apagado» y el usuario puede decidir si continúa o cancela.3
flowchart TB
accTitle: Flujo de protección con ShutdownBlockReasonCreate
accDescr: Al iniciar un proceso que no se puede interrumpir se registra el motivo, y si llega una solicitud de apagado durante la protección, el motivo se muestra a pantalla completa y se rechaza devolviendo FALSE a WM_QUERYENDSESSION. El usuario puede cancelar el apagado o forzar la continuación, y al terminar el proceso se libera el motivo
begin["Inicia un proceso que no se puede interrumpir"] --> reg["Registra el motivo con ShutdownBlockReasonCreate"]
reg --> work["Ejecuta el proceso (hilo de trabajo)"]
work --> done["Termina → libera con ShutdownBlockReasonDestroy"]
req["Solicitud de apagado durante este intervalo"] --> show["Se muestra el motivo en la pantalla «impide el apagado» + FALSE a WM_QUERYENDSESSION"]
show -->|"El usuario cancela"| work
show -->|"El usuario fuerza la continuación"| kill["Fin del proceso (aun así no queda protegido del todo)"]
[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);
// Debe llamarse desde el hilo que creó la ventana principal (falla si se llama desde otro hilo)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Se están escribiendo los datos de medición en el archivo");
try
{
// El proceso que no se puede interrumpir se ejecuta en un hilo de trabajo. Si se
// ejecuta de forma síncrona en el hilo de la UI, se detiene el bucle de mensajes y
// el código de rechazo de WM_QUERYENDSESSION de abajo no llega a actuar antes de
// que se fuerce la continuación por considerarse "sin respuesta"
await Task.Run(() => WriteMeasurementData());
}
finally
{
ShutdownBlockReasonDestroy(this.Handle);
_criticalOperationInProgress = false;
}
// Además, mientras dura la protección, se devuelve FALSE a WM_QUERYENDSESSION para rechazar
protected override void WndProc(ref Message m)
{
const int WM_QUERYENDSESSION = 0x0011;
if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
{
m.Result = IntPtr.Zero; // Rechazo. Se muestra el texto de motivo registrado en la UI a pantalla completa
return;
}
base.WndProc(ref m);
}
Aquí es fácil confundir el reparto de funciones. Lo único que hace ShutdownBlockReasonCreate es registrar el texto de motivo, y por sí solo no detiene el apagado. Lo que realmente lo frena es, como en el ejemplo de arriba, el propio proceso que activa un indicador de protección y devuelve FALSE a WM_QUERYENDSESSION. Se usan ambos juntos, y en cuanto termina el proceso se liberan los dos de inmediato. Además, el propio proceso protegido debe ejecutarse en un hilo de trabajo, manteniendo el hilo de la UI en condiciones de seguir procesando mensajes ── porque el mecanismo de rechazo solo funciona cuando el mensaje llega a procesarse (aun así, tanto el usuario como el sistema operativo pueden forzar la continuación, así que el diseño de escritura que no se rompe «cuando no se logra detener» — capítulo 8 — sigue siendo necesario).
Hay tres puntos de cuidado en la operación:
- El texto de motivo debe ser corto y concreto. El usuario tiene prisa y solo lo lee unos segundos. La propia documentación pone como ejemplo algo del estilo de «Se está grabando el CD».3
- No dejarlo registrado todo el tiempo que la aplicación está en ejecución. La API está pensada para «solo mientras dura el proceso que no se puede interrumpir».
- No diseñar dando por hecho que se puede bloquear. El usuario puede elegir forzar la continuación, y en un apagado forzado (ENDSESSION_CRITICAL) directamente no se espera en absoluto. Se indica explícitamente que «la aplicación no debe depender de poder bloquear el apagado».4
5. Las buenas prácticas en apps de consola y procesos en segundo plano
5.1. SetConsoleCtrlHandler y su margen corto
Las aplicaciones de consola no pueden recibir mensajes de ventana, así que la señal de control llega a la función controladora registrada con SetConsoleCtrlHandler. El margen predeterminado de cada señal es el siguiente.5
| Señal | Cuándo ocurre | Margen predeterminado |
|---|---|---|
| CTRL_C_EVENT / CTRL_BREAK_EVENT | Ctrl+C / Ctrl+Break | Sin tiempo de espera |
| CTRL_CLOSE_EVENT | Cerrar la consola, «finalizar tarea» del Administrador de tareas (la terminación forzada de proceso desde la pestaña «Detalles» es una terminación inmediata sin notificación, fuera del alcance de esta tabla) | Unos 5 s |
| CTRL_SHUTDOWN_EVENT | Apagado del sistema (proceso de servicio) | Unos 20 s |
Hay dos puntos a tener en cuenta. Primero, CTRL_LOGOFF_EVENT y CTRL_SHUTDOWN_EVENT solo los recibe, en la práctica, un proceso que se ejecuta como servicio. Una aplicación dentro de una sesión interactiva se termina en el momento del cierre de sesión, así que un diseño que espere esta señal no es viable.5 Segundo, un proceso que cargó gdi32.dll o user32.dll se trata como una aplicación de Windows aunque pretenda ser una app de consola, y sus manejadores de tipo LOGOFF/SHUTDOWN no se llaman. El mecanismo oficial para sortear esto es crear una ventana oculta y recibir WM_QUERYENDSESSION/WM_ENDSESSION.6
flowchart TB
accTitle: Margen de cada señal de consola
accDescr: CTRL_C y CTRL_BREAK no tienen un tiempo de espera explícito, el cierre de consola da unos 5 segundos y la señal de apagado a un proceso de servicio da unos 20 segundos; superado ese margen se fuerza la terminación
ctrlc["CTRL_C / CTRL_BREAK"] -->|"sin tiempo de espera"| handler["Cierre en el HandlerRoutine registrado"]
closeev["CTRL_CLOSE_EVENT"] -->|"unos 5 s"| handler
shutev["CTRL_SHUTDOWN_EVENT (proceso de servicio)"] -->|"unos 20 s"| handler
handler --> timeout["Al superar el margen se fuerza la terminación"]
// Aplicación de consola: se hace el cierre con 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; // Se mantiene para evitar que el GC lo recolecte
static bool OnCtrlEvent(int ctrlType)
{
// Solo se hace el cierre que termina en menos de 5 segundos
FlushAndCloseDataFile();
return false; // Pasa al manejador predeterminado y el proceso termina
}
static void Main()
{
SetConsoleCtrlHandler(s_handler, add: true);
// ...
}
5.2. La trampa de .NET ── no depender de ProcessExit
Durante mucho tiempo, en .NET la práctica habitual era «basta con hacer el cierre en AppDomain.ProcessExit», pero desde .NET 10 el runtime ya no ofrece un manejador de señal de terminación predeterminado, y con CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT ya no se dispara ni ProcessExit ni AssemblyLoadContext.Unloading. El manejador predeterminado del sistema operativo se limita a terminar el proceso de inmediato.7
flowchart LR
accTitle: El cambio de comportamiento de ProcessExit en .NET 10
accDescr: Hasta .NET 9, el manejador de señal predeterminado del runtime recibía la señal de terminación, disparaba ProcessExit y luego terminaba. Desde .NET 10 el runtime ya no ofrece un manejador predeterminado y el procesamiento predeterminado del sistema operativo termina el proceso de inmediato, así que hay que registrar el propio manejador
sig["CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT"] --> old9["Hasta .NET 9: manejador predeterminado del runtime → se dispara ProcessExit → termina"]
sig --> new10["Desde .NET 10: sin manejador predeterminado → el SO termina de inmediato (sin ProcessExit)"]
new10 -.-> alt["Solución: registrar SetConsoleCtrlHandler / PosixSignalRegistration por cuenta propia"]
En su lugar, hay que trasladarse a la ruta oficial de cada modelo de aplicación.
- Apps GUI: FormClosing / SessionEnding del capítulo anterior
- Generic Host (incluido Worker Service): IHostApplicationLifetime y BackgroundService.StopAsync. El margen de detención se declara explícitamente con HostOptions.ShutdownTimeout
- Apps de consola puras: SetConsoleCtrlHandler (o suscribirse al equivalente de SIGINT/SIGTERM con PosixSignalRegistration)
flowchart TB
accTitle: Punto de recepción de la notificación de cierre según el modelo de aplicación
accDescr: Las apps GUI reciben FormClosing y SessionEnding, y el proceso de confirmación mediante un hook de WM_ENDSESSION; el Generic Host recibe IHostApplicationLifetime y StopAsync; la consola pura recibe SetConsoleCtrlHandler o PosixSignalRegistration. Confiar en ProcessExit no funciona en las rutas de señal externa
model{"¿Qué modelo de aplicación es?"}
model -->|"GUI (WinForms/WPF)"| gui["FormClosing / SessionEnding (la confirmación va por un hook de WM_ENDSESSION)"]
model -->|"Generic Host / Worker Service"| host["IHostApplicationLifetime + StopAsync (declarar ShutdownTimeout)"]
model -->|"Consola pura"| con["SetConsoleCtrlHandler / PosixSignalRegistration"]
ng["Depender de AppDomain.ProcessExit"] -.-> ngx["✕ Desde .NET 10 no se dispara en las rutas de señal externa"]
El margen varía según la ruta ── el cierre de consola o de una app GUI da unos 5 segundos, un servicio tiene el margen del SCM del capítulo 6 (unos 20 segundos, o el valor configurado si es PRESHUTDOWN), y Ctrl+C no tiene un tiempo de espera explícito. Pero en cualquier ruta el margen es limitado y no se puede dar por seguro, así que el eje del diseño no debe ser «esforzarse en el evento de terminación», sino que lo normal sea ya haber guardado en cada hito del proceso.
6. Las buenas prácticas en servicios de Windows ── SHUTDOWN y PRESHUTDOWN
6.1. Dos tipos de notificación de apagado
Los servicios no se ven afectados por el cierre de sesión, pero sí se detienen con el apagado o el reinicio. La notificación llega desde el Administrador de Control de Servicios (SCM) como un código de control, y para recibirla hace falta declarar el indicador de aceptación correspondiente.8
| Declaración | Notificación que llega | Momento y margen |
|---|---|---|
| SERVICE_ACCEPT_SHUTDOWN | SERVICE_CONTROL_SHUTDOWN | Se notifica durante el propio proceso de apagado. Por defecto, unos 20 segundos, con WaitToKillServiceTimeout como límite superior |
| SERVICE_ACCEPT_PRESHUTDOWN | SERVICE_CONTROL_PRESHUTDOWN | Se notifica antes que SHUTDOWN. El SCM espera hasta que el servicio se detenga o hasta que se agote el tiempo de espera |
flowchart LR
accTitle: Orden de las notificaciones de apagado a los servicios
accDescr: Al iniciarse el apagado, primero se notifica con el margen configurado a los servicios que declararon PRESHUTDOWN, luego se envía la notificación SHUTDOWN con un margen predeterminado de unos 20 segundos, y al agotarse el margen se termina el proceso
start["Inicio del apagado"] --> pre["SERVICE_CONTROL_PRESHUTDOWN (solo servicios que lo declararon · margen configurado)"]
pre --> shut["SERVICE_CONTROL_SHUTDOWN (predeterminado: unos 20 s)"]
shut --> kill["Se agota el margen → fin del proceso"]
El tiempo de espera de PRESHUTDOWN se puede configurar con ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO), y el valor predeterminado es de 10 segundos desde Windows 10 Creators Update (compilación 15063) en adelante, y de 3 minutos en versiones anteriores.9 Si se sigue partiendo del conocimiento antiguo de «con PRESHUTDOWN se dispone de 3 minutos», en el sistema operativo actual solo se tiene 1/18 del margen esperado. Además, como PRESHUTDOWN hace esperar a todo el apagado del sistema durante ese tiempo, la propia documentación indica que «solo debe usarse en situaciones especiales».8
El comportamiento del lado del manejador también es importante. El manejador de control debe devolver el control en menos de 30 segundos, así que el proceso de detención que tarda debe delegarse a otro hilo, mientras el manejador informa SERVICE_STOP_PENDING y regresa de inmediato.8
// Servicio Win32: acepta PRESHUTDOWN y delega el proceso 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); // Indica al trabajador que se detenga y regresa de inmediato
return NO_ERROR;
}
return ERROR_CALL_NOT_IMPLEMENTED;
}
// Lado del trabajador: si el cierre se alarga más que el waitHint, hay que seguir
// informando SERVICE_STOP_PENDING de forma periódica mientras se incrementa
// dwCheckPoint. El SCM interpreta el waitHint y el avance del checkpoint como
// "sigue vivo y avanzando". Si dejan de llegar informes, se considera colgado
// y el apagado puede seguir adelante. Al terminar, hay que informar siempre SERVICE_STOPPED
6.2. Un diseño que no depende del margen de gracia
Reescribir en el lado del servicio el WaitToKillServiceTimeout, el límite superior del margen, para ampliarlo es algo claramente desaconsejado. La documentación exige más bien lo contrario: que el servicio termine el cierre lo más rápido posible, para que una máquina alimentada por un SAI pueda completar el apagado antes de que se agote la batería. La directriz es guardar con frecuencia en el día a día para minimizar los datos sin guardar, no dedicar tiempo a cosas como liberar memoria en el momento del apagado, y no esperar demasiado la respuesta de las notificaciones a interlocutores de red. Además, como el SCM en el apagado notifica por defecto sin tener en cuenta las dependencias, el proceso de detención también debe estar preparado para «no romperse aunque el servicio del que depende ya se haya detenido antes».8
flowchart TB
accTitle: Diseño del proceso de parada que no depende del margen de gracia
accDescr: Si se guarda en cada hito del proceso y los datos sin guardar se mantienen siempre al mínimo, el cierre al recibir la notificación de parada termina en segundos. Un diseño que guarda todo junto al final no cabe en el margen y pierde datos al forzarse la terminación
good["Habitual: guardar en cada hito (datos sin guardar siempre al mínimo)"] --> gstop["Notificación de parada → se guarda lo poco que resta → termina en segundos"]
bad["Habitual: acumular en memoria y guardar todo junto al final"] --> bstop["Notificación de parada → el guardado no cabe en el margen"]
bstop --> killed["Terminación forzada → pérdida de datos"]
En el Worker Service de .NET (UseWindowsService), SERVICE_CONTROL_STOP y SHUTDOWN se traducen en la detención del host, y se llama a BackgroundService.StopAsync. En el momento de escribir este artículo, la implementación estándar acepta la familia STOP/SHUTDOWN, y si hace falta llegar hasta PRESHUTDOWN se necesita una implementación de manejador extendida. En cualquier caso, lo básico es declarar explícitamente HostOptions.ShutdownTimeout y hacer que StopAsync termine en segundos. Para el diseño general de servicios, consulte «Cómo crear y operar servicios de Windows».
7. Recuperación automática tras el reinicio
En un PC industrial o en un PC que funciona sin supervisión, el alcance del diseño no termina en «sobrevivir al apagado», sino que también incluye «recuperarse por sí solo tras el reinicio».
7.1. RegisterApplicationRestart y la retrollamada de recuperación
Al llamar a RegisterApplicationRestart, la aplicación queda registrada como candidata a reiniciarse en cada uno de estos casos: fallo (excepción no controlada), falta de respuesta, reinicio de la aplicación por actualización y reinicio del sistema operativo por actualización. Como se pueden registrar los argumentos de línea de comandos para el reinicio, si se incluye en ellos «qué archivo estaba abierto» o «cuál es el punto de restauración», se puede continuar justo donde se dejó tras el reinicio.10
Las especificaciones que hay que tener presentes son las siguientes.10
- El registro debe completarse antes de que ocurra el problema (en el escenario de actualización, la última oportunidad es durante el procesamiento de WM_QUERYENDSESSION)
- Para evitar bucles de reinicio, los procesos con menos de 60 segundos desde el arranque no se reinician
- Los procesos que se ejecutan elevados a administrador no son candidatos al reinicio automático (porque no se puede recrear el proceso sin el consentimiento de la elevación). En las aplicaciones que necesitan elevación, la recuperación automática se diseña dejando la UI con privilegios estándar y separando el trabajo con privilegios en un servicio, o mediante una ruta de arranque explícita, como una tarea del Programador de tareas configurada para «ejecutar con los privilegios más altos»
- El reinicio por fallo o por falta de respuesta se hace con el consentimiento del usuario, mientras que el reinicio por actualización es automático
- Para que la recuperación se mantenga a través de un reinicio del sistema operativo, quien ordena el reinicio (por ejemplo, un instalador) debe llamar a la API de apagado con los indicadores EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Si además se registra RegisterApplicationRecoveryCallback, en caso de fallo el WER (informe de errores de Windows) llama a esa retrollamada y da un margen para guardar los datos en curso. Ahora bien, si el guardado tarda, hay que seguir llamando a ApplicationRecoveryInProgress dentro del intervalo de ping indicado en el registro, o el proceso de recuperación se interrumpe a mitad de camino. Al terminar el guardado, se notifica la finalización con ApplicationRecoveryFinished. La «sustitución de un exe/DLL en uso y su reinicio» al actualizar una aplicación es competencia de Restart Manager, y se trata en detalle en «Cómo sustituir un exe/DLL en uso».
7.2. ARSO ── inicio de sesión automático tras el reinicio por actualización
Tras un reinicio provocado por Windows Update, si nadie inicia sesión, las aplicaciones de la sesión de usuario no vuelven. ARSO (inicio de sesión automático de reinicio de Winlogon) cubre este vacío. Cuando Windows Update inicia el reinicio, guarda de forma segura las credenciales del último usuario interactivo y configura el inicio de sesión automático, de modo que tras el reinicio inicia sesión automáticamente con ese usuario y a continuación bloquea la pantalla.11 También existen comandos como shutdown /g, que ordenan reiniciar y reanudar las aplicaciones registradas. Como algunos entornos lo tienen desactivado por directiva de organización (por ejemplo, DisableAutomaticRestartSignOn), al diseñar una recuperación sin supervisión conviene comprobar también este ajuste. Dicho esto, si un proceso en segundo plano siempre necesario depende del arranque automático en la sesión de usuario, lo coherente desde el principio es convertirlo en un servicio de Windows.
flowchart TB
accTitle: Ruta hasta que la aplicación vuelve automáticamente tras el reinicio
accDescr: Si se registra de antemano con RegisterApplicationRestart, en caso de fallo o falta de respuesta la aplicación se reinicia con el consentimiento del usuario, y en el reinicio por actualización se reinicia tras el inicio de sesión automático y el bloqueo de pantalla de ARSO. Quedan excluidos los procesos con menos de 60 segundos de actividad y los procesos elevados
reg["Registro con RegisterApplicationRestart (antes de que ocurra el problema)"]
reg --> crash["Fallo · sin respuesta"]
reg --> update["Reinicio por actualización"]
crash -->|"Consentimiento del usuario"| restart["Reinicio de la aplicación"]
update --> arso["ARSO: inicio de sesión automático + bloqueo de pantalla"]
arso --> restart
restart -.-> limits["Excluidos: menos de 60 s de actividad (evita bucles), procesos elevados"]
8. Resistir el corte de energía sin notificación ── diseño de escritura y SAI
8.1. Una escritura que «no se rompe pase lo que pase» ── archivo temporal + ReplaceFile
Un corte del disyuntor, una avería de la fuente de alimentación o desenchufar el equipo no tienen ni WM_ENDSESSION ni PRESHUTDOWN. Mientras el archivo de configuración o los resultados de medición se «sobrescriban directamente en el archivo original», un corte de energía a mitad de la escritura puede dejar un archivo dañado que mezcla lo antiguo y lo nuevo.
Lo habitual es escribir por completo en un archivo temporal de la misma unidad y luego sustituirlo. ReplaceFile agrupa en una sola API la secuencia de pasos «guardar en el archivo nuevo → apartar el archivo original → renombrar → eliminar», y también conserva atributos del archivo original como la fecha de creación, la ACL o los flujos alternativos (los tres archivos deben estar en la misma unidad).12 File.Replace de .NET llama a esto directamente.
flowchart TB
accTitle: Flujo de guardado y recuperación con archivo temporal y ReplaceFile
accDescr: Al guardar se escribe por completo en un archivo temporal, se vacía el búfer y se sustituye con ReplaceFile, dejando el contenido anterior en .bak. Al arrancar se verifica el archivo principal y, si está dañado, se recurre al .bak
subgraph save["Al guardar"]
w["Escribe por completo en un archivo temporal"] --> f["Vacía el búfer (equivalente a FlushFileBuffers)"]
f --> r["Sustituye con ReplaceFile (el contenido anterior va a .bak)"]
end
subgraph startup["En el próximo arranque"]
v["Verifica el archivo principal"]
v -->|"correcto"| use["Se usa tal cual"]
v -->|"dañado"| bak["Recurre al .bak"]
end
r -.->|"sin importar en qué instante ocurra el corte de energía"| v
// Práctica habitual para guardar configuración o datos: escribir por completo en un
// archivo temporal antes de sustituir, conservando también el contenido anterior
public static void SaveAtomically(string path, string content)
{
string dir = Path.GetDirectoryName(path)!;
string tmp = Path.Combine(dir, Path.GetRandomFileName()); // Se crea en la misma unidad
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. Envía el búfer del SO
// al disco (el límite de la caché del propio dispositivo se trata en 8.2)
}
if (File.Exists(path))
File.Replace(tmp, path, path + ".bak"); // Llama a ReplaceFile. Conserva el contenido anterior como .bak
else
File.Move(tmp, path);
}
catch
{
// Si falla a mitad de camino, no se deja el archivo temporal. Si los fallos se
// repiten en el guardado periódico, copias completas irían llenando la unidad
try { File.Delete(tmp); } catch { /* Se prioriza la excepción original si falla el borrado */ }
throw;
}
}
Con esto, en el funcionamiento normal siempre queda disponible para lectura, o bien un «archivo antiguo completo», o bien un «archivo nuevo completo». Sin embargo, ReplaceFile es una operación de espacio de nombres de varios pasos, y su atomicidad ante un corte de energía no está garantizada por especificación. Precisamente por eso, en el ejemplo anterior se conserva una copia de seguridad (.bak) ── el lado de lectura debe implementar también, como parte del mismo conjunto, la verificación del archivo principal en el arranque y el retorno a la copia de seguridad si está dañado. Esto no sirve para un registro o un CSV de tipo anexado, así que en esos casos conviene usar un formato que ya prevea cómo puede romperse, por ejemplo «escribir una línea por registro y descartar la última línea dañada al leer».
8.2. Que WriteFile tenga éxito no significa que haya llegado al disco
Otra premisa importante es que aunque WriteFile devuelva éxito, los datos puede que todavía estén solo en la caché del sistema operativo. Windows sitúa las lecturas y escrituras de archivos en un búfer del sistema y las refleja en el disco periódicamente mediante escritura diferida. Para garantizar que llegan al disco, hay que vaciar el búfer explícitamente con FlushFileBuffers, o indicar FILE_FLAG_WRITE_THROUGH al llamar a CreateFile para que la caché se salte en cada escritura. Además, como los metadatos del sistema de archivos siempre se almacenan en caché, confirmarlos también requiere un vaciado o el modo write-through.13
Ahora bien, llamar a FlushFileBuffers en cada operación es ineficiente, y la propia documentación sugiere considerar FILE_FLAG_NO_BUFFERING + WRITE_THROUGH en lugar de llamadas frecuentes.13 En la práctica, un punto de equilibrio razonable es «vaciar solo en los hitos de una transacción y justo antes de cerrar el archivo». Esta capa ── el administrador de caché, la escritura diferida y el hecho de que «aunque se haya vaciado, puede que no haya llegado al disco» por culpa de la caché del propio hardware ── se profundiza en «El administrador de caché: cuándo llega su WriteFile al disco».
8.3. SAI y monitoreo de batería ── convertir un corte de energía en un apagado
La medida definitiva contra el corte de energía en un PC industrial es el SAI. Conviene entender que el papel del SAI no es «impedir el apagón», sino convertir un «corte de energía sin notificación» en un «apagado planificado con notificación». El diseño se apoya en dos frentes.
- Diseño del margen: el tiempo de autonomía de la batería del SAI debe ser mayor que la suma de «detección del cambio a batería + cierre de la aplicación y del servicio + finalización del apagado del sistema operativo». Si el proceso de detención del servicio es lento, esta ecuación deja de cumplirse (sección 6.2)
- Detección: el cambio de alimentación de CA a batería, o la caída del nivel de carga, se notifica con el evento PBT_APMPOWERSTATUSCHANGE. Una aplicación con ventana lo recibe mediante WM_POWERBROADCAST, y un servicio sin ventana lo recibe como SERVICE_CONTROL_POWEREVENT en HandlerEx tras declarar SERVICE_ACCEPT_POWEREVENT (WM_POWERBROADCAST no llega al manejador de control de un servicio). Al recibirlo, hay que llamar a GetSystemPowerStatus y comprobar ACLineStatus (si hay alimentación de CA) y BatteryLifePercent, para enlazar con la interrupción de la medición, el guardado y la solicitud de apagado15
flowchart LR
accTitle: Flujo con el que el SAI convierte un corte de energía en un apagado planificado
accDescr: Cuando un corte eléctrico hace que el SAI pase a alimentación por batería, se notifica PBT_APMPOWERSTATUSCHANGE, se verifica el estado de energía y se enlaza con el guardado y la solicitud de apagado, con lo que un corte de energía sin notificación pasa a ser un apagado planificado con notificación
outage["Corte eléctrico · corte de energía"] --> ups["El SAI cambia a alimentación por batería"]
ups --> pbt["Notificación PBT_APMPOWERSTATUSCHANGE"]
pbt --> check["Verifica el estado con GetSystemPowerStatus"]
check --> saveop["Interrumpe y guarda la medición"]
saveop --> req["Solicita el apagado al SO"]
req --> normal["Flujo normal de notificación de apagado (capítulos 3 a 6)"]
Los SAI genéricos conectados por USB se ven desde Windows como una batería, así que se pueden detectar con esta API estándar. Si el software de gestión del fabricante tiene una función que «apaga el sistema operativo al llegar a N % de carga», conviene también verificar la coherencia entre ese umbral y el tiempo que tarda el cierre de la propia aplicación. Por otro lado, el problema de la recuperación desde la suspensión o la hibernación en aplicaciones de funcionamiento prolongado se trata como un eje aparte en «Suspensión, hibernación, Modern Standby y aplicaciones de larga duración».
9. Cómo verificarlo ── probar el apagado con seguridad
El código de manejo del apagado tiende a quedarse como «se escribió, pero nunca se probó en condiciones equivalentes a producción». Conviene tener preparado un procedimiento para verificarlo con seguridad.
- Probar en una máquina de verificación o virtual: no probar directamente en el PC industrial de producción; en un entorno de verificación del que se haya tomado un punto de control (instantánea) con Hyper-V u otra herramienta similar, se repiten el apagado, el reinicio y el corte de energía forzado (apagar la VM). Ahora bien, «apagar» una VM solo reproduce que «el sistema operativo invitado se detiene sin previo aviso», y no reproduce la pérdida de la caché volátil del disco físico ni las formas de daño que dependen del controlador. Si el producto se va a distribuir como un PC industrial, la verificación final debe hacerse cortando realmente la energía en hardware equivalente al de producción
- Confirmación sencilla con el cierre de sesión: la ruta WM_QUERYENDSESSION → WM_ENDSESSION también pasa por el cierre de sesión (la única diferencia es que se activa el bit ENDSESSION_LOGOFF del lParam), así que se puede comprobar cómodamente el funcionamiento del código de cierre en una máquina de desarrollo1
- Distinguir entre apagado completo e híbrido al probar: se pueden probar por separado
shutdown /s /t 0(completo),shutdown /s /hybrid /t 0(comportamiento predeterminado) yshutdown /r /t 0(reinicio)2 - Medir el tiempo que tarda el cierre: se escribe la hora en un registro al principio y al final de la función de cierre, y se comprueba de forma real si cabe en los 5 segundos (o en el margen configurado, en el caso de un servicio)
flowchart LR
accTitle: Operaciones de verificación y qué se puede comprobar con cada una
accDescr: El cierre de sesión permite comprobar fácilmente la ruta de notificación, los comandos shutdown completo, híbrido y de reinicio comprueban la ruta y el margen de producción, el apagado de la VM comprueba la resistencia a una parada repentina, y la prueba de corte de energía en hardware real comprueba de forma definitiva la resistencia incluyendo el almacenamiento físico
signtest["Cierre de sesión"] -.-> path1["Ruta WM_QUERYENDSESSION → WM_ENDSESSION (comprobación fácil)"]
shuttest["shutdown /s · /s /hybrid · /r"] -.-> path2["Ruta de notificación y margen de producción"]
vmtest["Apagado de la VM"] -.-> path3["Resistencia a una parada repentina del invitado"]
hwtest["Prueba de corte de energía en hardware real"] -.-> path4["Resistencia incluyendo el almacenamiento físico (comprobación final)"]
Para el diagnóstico posterior se puede usar el registro de eventos (System). En un apagado o reinicio normal se registra el ID de evento 1074 (qué proceso inició el apagado, para quién y por qué motivo). En un corte de energía repentino o un fallo no aparece el 1074, y en el siguiente arranque se registran el ID de evento 41 (Kernel-Power) y el 6008 (el apagado anterior no fue previsto).14 «Qué pasó en mitad de la noche» es lo primero que hay que consultar aquí.
# Consultar 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 el 1074 indica «reinicio por Windows Update» y aun así los datos de la aplicación estaban dañados, el problema está en el código de cierre. Lo que indican el 6008/41 es solo «un apagado no previsto»: se registran tanto en un corte de energía como en una pantalla azul (fallo) o un reinicio forzado. Si el BugcheckCode del 41 es distinto de 0, es un fallo; si es 0 y tampoco queda un volcado de memoria, lo más probable es un corte de energía; así se acota la causa con la información del entorno, y una vez identificado el corte de energía entran en juego el diseño de escritura y el SAI del capítulo 8.
10. Resumen
- El apagado es «un evento normal que siempre llega, tarde o temprano». El margen tras la notificación es, en principio, de solo unos 5 segundos, así que la base es minimizar de antemano, con un autoguardado frecuente, «lo que hay que hacer al terminar».
- En los sistemas operativos cliente desde Windows 8, si el inicio rápido está activado, «Apagar» es un apagado híbrido y el núcleo solo está hibernando. El único reinicio completo es «Reiniciar» ── escriba «reiniciar» en los procedimientos de respuesta a fallos.
- Las apps GUI devuelven TRUE de inmediato a WM_QUERYENDSESSION, y el cierre posterior a la confirmación se hace en WM_ENDSESSION. FormClosing y SessionEnding de WinForms/WPF corresponden a la fase de consulta, así que ahí solo se debe hacer un guardado de instantánea idempotente. No se debe mostrar ningún diálogo durante el apagado.
- Un proceso que de verdad no se puede interrumpir se protege mostrando el motivo con ShutdownBlockReasonCreate. Aun así, no hay ninguna garantía de poder bloquear el apagado.
- Las apps de consola reciben la notificación con SetConsoleCtrlHandler, y los servicios con SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. El margen predeterminado de PRESHUTDOWN es de 10 segundos en el sistema operativo actual. En .NET, hay que dejar de depender de ProcessExit y trasladarse a la ruta oficial de cada modelo de aplicación.
- La recuperación tras el reinicio se puede automatizar por completo con RegisterApplicationRestart (más la retrollamada de recuperación) y ARSO.
- Un corte de energía no trae ninguna notificación. Se afronta con la sustitución mediante archivo temporal + ReplaceFile (junto con copia de seguridad y verificación en el arranque), con el vaciado en los hitos y con el SAI, que convierte el corte de energía en un apagado planificado.
flowchart TB
accTitle: Panorama general de la gestión del apagado
accDescr: A los finales con notificación se responde con un cierre que termina en segundos, que enlaza con la recuperación automática tras el reinicio, y para el corte de energía sin notificación se prepara con una escritura que no se rompe pase lo que pase y con un SAI, verificando incluso con hardware real. Estos dos pilares son la conclusión del artículo
ending{"Tipo de final"}
ending -->|"Con notificación (cierre de sesión, apagado, reinicio)"| pillar1["Cierre que termina en segundos (capítulos 3 a 6)"]
ending -->|"Sin notificación (corte de energía)"| pillar2["Escritura que no se rompe pase lo que pase + SAI (capítulo 8)"]
pillar1 --> recover["Recuperación automática tras el reinicio (capítulo 7)"]
pillar2 --> verifytest["Verificación incluyendo hardware real (capítulo 9)"]
- La verificación se hace con seguridad en una máquina virtual y con el cierre de sesión, y el diagnóstico posterior se hace con los ID de evento 1074/41/6008.
La próxima vez que agregue una función a su aplicación, hágase esta pregunta, aunque solo sea una vez: si llega WM_ENDSESSION justo en medio de este proceso, o si se corta la energía, ¿qué quedará en el siguiente arranque? Escribir esa respuesta en el diseño es el camino más corto para eliminar «esos días en que uno se queda con la cabeza entre las manos frente al PC industrial por la mañana».
Artículos relacionados
- Cómo sustituir un exe/DLL en uso ── Restart Manager y el problema del “archivo en uso” en las actualizaciones automáticas
- Cómo crear y operar servicios de Windows ── de cuándo usar el Programador de tareas a convertir un BackgroundService en servicio
- Suspensión, hibernación, Modern Standby y aplicaciones de larga duración ── cómo evitar con el diseño que “se quedara detenido a medianoche”
- Las profundidades de la E/S de Windows (4.ª entrega) ── el administrador de caché: cuándo llega su WriteFile al disco
- Cómo dejar registros y volcados al fallar una aplicación de Windows
- Lista de comprobación para manejar procesos hijos con seguridad en una aplicación de Windows
Áreas de consultoría relacionadas
En KomuraSoft LLC nos ocupamos del diseño e implementación de medidas de apagado y corte de energía para PC industriales y aplicaciones de funcionamiento prolongado, de la investigación de causas de corrupción de datos y de incidentes de “estaba detenido por la mañana” a raíz de reinicios de Windows Update o cierres de sesión, y de la revisión de diseño del proceso de detención y la recuperación automática de servicios de Windows. Puede empezar desde la fase de “tengo la sensación de que algo se rompe cada vez que se apaga, pero no sé por dónde empezar”.
- Desarrollo de aplicaciones de Windows
- Investigación de fallos y análisis de causas
- Consultoría técnica y revisión de diseño
- Contacto
Referencias
</content>
-
Microsoft Learn, WM_QUERYENDSESSION message. Sobre que WM_QUERYENDSESSION se envía al terminar la sesión, que la aplicación debe devolver TRUE respetando la intención del usuario (la respuesta predeterminada de DefWindowProc también es TRUE), que el cierre debe retrasarse hasta WM_ENDSESSION, que a los 5 segundos el sistema muestra la UI de la aplicación que está impidiendo el apagado y el usuario puede forzar la terminación, el significado de los bits ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL del lParam, que el apagado y el reinicio no se pueden distinguir, y que se deben guardar los datos con frecuencia para reducir la cantidad que hay que guardar al terminar. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. Sobre que con el inicio rápido la sesión del núcleo no se cierra y se trata como una hibernación, guardándose el estado del núcleo y de los controladores de dispositivo en hiberfil.sys; que “Reiniciar” siempre hace un arranque completo porque necesita un estado de Windows totalmente nuevo; que el inicio rápido está activado por defecto y no se recomienda desactivarlo; y que el comportamiento predeterminado de Shutdown.exe es el apagado completo, mientras que la opción /hybrid da el comportamiento híbrido. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). Sobre llamarla al iniciar un proceso que no se puede interrumpir para registrar el texto de motivo, y llamar a ShutdownBlockReasonDestroy al terminar; que solo se puede llamar desde el hilo que creó la ventana; y que el motivo debe ser corto y claro porque el usuario solo lo lee unos segundos. ↩ ↩2 ↩3
-
Microsoft Learn, Shutdown Changes for Windows Vista. Sobre que la respuesta a WM_QUERYENDSESSION/WM_ENDSESSION solo se puede retrasar 5 segundos cada una, tras lo cual el usuario puede elegir continuar o cancelar; que una aplicación de consola o sin ventana visible no puede cancelar el apagado y se termina automáticamente si no responde en 5 segundos o responde FALSE; que si hace falta bloquear, se debe registrar el motivo con ShutdownBlockReasonCreate; y que la aplicación no debe depender de poder bloquear el apagado. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, HandlerRoutine callback function. Sobre los eventos CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN que recibe el manejador registrado con SetConsoleCtrlHandler, que el tiempo de espera predeterminado de CTRL_CLOSE_EVENT es de unos 5000 milisegundos y el de CTRL_SHUTDOWN_EVENT en un proceso de servicio es de unos 20 000 milisegundos, que CTRL_LOGOFF/SHUTDOWN_EVENT en la práctica solo los reciben los servicios porque una app interactiva se termina en el momento del cierre de sesión, y que el manejador se ejecuta en un hilo aparte. ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. Sobre que un proceso que cargó gdi32.dll o user32.dll se trata como una aplicación de Windows y no se le llaman los manejadores de CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT; que como solución alternativa se debe crear una ventana oculta y procesar WM_QUERYENDSESSION/WM_ENDSESSION; y que las funciones de consola pueden no funcionar con normalidad mientras se procesa la señal. ↩ ↩2
-
Microsoft Learn, .NET runtime no longer provides default termination signal handlers. Sobre que desde .NET 10 el runtime deja de ofrecer un manejador predeterminado para CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT de Windows (equivalente a SIGTERM/SIGHUP en Unix); que el procesamiento predeterminado del sistema operativo termina la aplicación de inmediato y ya no se disparan AppDomain.ProcessExit ni AssemblyLoadContext.Unloading; y que el manejo de señales adecuado para cada modelo de aplicación se debe registrar en bibliotecas de nivel superior o en el propio código de la aplicación. ↩ ↩2
-
Microsoft Learn, Service Control Handler Function. Sobre que un servicio que declaró SERVICE_ACCEPT_PRESHUTDOWN recibe primero SERVICE_CONTROL_PRESHUTDOWN, y después los servicios con SERVICE_ACCEPT_SHUTDOWN reciben SERVICE_CONTROL_SHUTDOWN; que el margen predeterminado en el apagado es de unos 20 segundos y el límite superior en el reinicio del sistema operativo es WaitToKillServiceTimeout, valor que no se debe ampliar; que el manejador de control debe devolver el control en menos de 30 segundos, informando STOP_PENDING y una pista de espera, y delegando los procesos largos a otro hilo; que se debe terminar el cierre lo más rápido posible teniendo en cuenta el funcionamiento con SAI; y que el SCM en el apagado no tiene en cuenta las dependencias por defecto. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). Sobre que tras la notificación PRESHUTDOWN el SCM espera hasta que el servicio se detenga o hasta agotar el tiempo de espera; que el tiempo de espera predeterminado es de 10 segundos desde Windows 10 Creators Update (compilación 15063) en adelante, y de 3 minutos en versiones anteriores; que se configura con ChangeServiceConfig2; y que durante SERVICE_STOP_PENDING se puede seguir actualizando el estado. ↩ ↩2
-
Microsoft Learn, RegisterApplicationRestart function (winbase.h). Sobre que se puede registrar el reinicio para los escenarios de fallo, falta de respuesta, actualización y reinicio del equipo por actualización; que se pueden especificar los argumentos de línea de comandos del reinicio; que el registro debe hacerse antes de que ocurra el problema, y que en el escenario de actualización la última oportunidad es durante el procesamiento de WM_QUERYENDSESSION; que los procesos con menos de 60 segundos desde el arranque no se reinician; que el fallo y la falta de respuesta reinician con el consentimiento del usuario; y que para mantener la recuperación a través de un reinicio del sistema operativo hace falta apagar con EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2 ↩3
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Sobre que al iniciar un reinicio automático, Windows Update guarda las credenciales del último usuario interactivo y configura el inicio de sesión automático; que tras el reinicio se inicia sesión automáticamente con ese usuario y se bloquea la sesión; que las credenciales guardadas se eliminan tras iniciar sesión correctamente; y que se puede configurar mediante directiva de grupo (por ejemplo, DisableAutomaticRestartSignOn). ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). Sobre que ReplaceFile agrupa en una sola función varios pasos equivalentes a “guardar en el archivo nuevo, apartar temporalmente el archivo original, renombrar el archivo nuevo y eliminar el archivo original”; 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 que la copia de seguridad, el archivo a sustituir y el archivo de sustitución deben estar en la misma unidad. ↩ ↩2
-
Microsoft Learn, File Caching. Sobre que las escrituras se cargan por defecto en la caché del sistema y se reflejan en el disco mediante escritura diferida; que FILE_FLAG_WRITE_THROUGH escribe de inmediato en el disco; que FlushFileBuffers permite vaciar explícitamente; y que los metadatos del sistema de archivos siempre están en caché, por lo que confirmarlos también requiere un vaciado o el modo write-through. ↩ ↩2 ↩3
-
Microsoft Learn, Troubleshoot unexpected reboots using system event logs. Sobre que en un reinicio normal se registra el ID de evento 1074 (qué proceso inició el apagado, para quién y por qué motivo); que en un reinicio no previsto se registran el ID de evento 41 (Kernel-Power) y el 6008 (el apagado anterior no fue previsto); y que estos ID permiten distinguir el tipo de reinicio. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. Sobre que este evento se notifica mediante WM_POWERBROADCAST al cambiar entre batería y alimentación de CA o al bajar el nivel de carga; y que al recibirlo se debe llamar a GetSystemPowerStatus para comprobar ACLineStatus, BatteryFlag, BatteryLifePercent y el resto de campos de SYSTEM_POWER_STATUS. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
WPR/WPA en la práctica — Introducción al análisis de rendimiento del sistema para «todo el PC va lento»
Problemas como «todo el PC va lento» o «el arranque es lento», que el Administrador de tareas no explica, se investigan con WPR/WPA leyen...
Selección de la cuenta de un servicio de Windows — Cuándo usar LocalSystem, cuentas virtuales y gMSA
¿Todavía ejecuta sus servicios de Windows con LocalSystem? Compare permisos e identidad en red de las seis cuentas posibles y elija con p...
Cómo funciona la compatibilidad de aplicaciones en Windows ── el modo de compatibilidad, los shims y Compatibility Administrator para prolongar la vida de aplicaciones antiguas
Por qué funciona el modo de compatibilidad: el mecanismo de los shims (hooks de API), sus funciones típicas, el despliegue con Compatibil...
Cómo interpretar los códigos de error de Windows — la estructura de tres capas de Win32, HRESULT y NTSTATUS
Antes de buscar 0x80004005, descompóngalo. Explicamos la estructura de tres capas Win32/HRESULT/NTSTATUS, el patrón 0x8007xxxx y cómo inv...
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é?
- Cuando el inicio rápido está activado en un sistema operativo cliente de Windows 8 en adelante (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 núcleo y de los controladores se guarda en el archivo de hibernación y se restaura tal cual en el siguiente arranque. Es decir, la parte central del sistema operativo no se reinicia. En cambio, «Reiniciar» siempre realiza un arranque completo, por lo que los problemas de controladores o servicios sí se reinician. En los procedimientos de diagnóstico de fallos, indique «reiniciar» en lugar de «apagar y volver a encender». Si desea forzar un apagado completo mediante un comando, puede usar shutdown /s.
- ¿Se puede detener el apagado hasta que termine el proceso de guardado de la aplicación?
- Se puede conseguir una espera temporal, pero no se puede detener con seguridad. Si registra un texto de motivo con ShutdownBlockReasonCreate únicamente mientras dura un proceso que no se puede interrumpir, ese motivo aparecerá en la pantalla «esta aplicación está impidiendo el apagado» y el usuario podrá decidir si continúa o cancela. Sin embargo, el usuario puede elegir forzar la continuación, y en un apagado forzado o en un reinicio por actualización puede que ni siquiera se espere. Por eso, el enfoque correcto no es «bloquear», sino reducir los datos que se pueden perder mediante un autoguardado frecuente y diseñar un cierre que se complete en unos segundos a partir de la notificación de fin de sesión.
- El proceso de detención de mi servicio de Windows tarda mucho tiempo. ¿Se puede ampliar el margen de gracia en el apagado?
- En la configuración predeterminada, en la que se recibe la notificación con SERVICE_CONTROL_SHUTDOWN, el margen es de unos 20 segundos y depende del valor de registro WaitToKillServiceTimeout. No se recomienda que la propia aplicación reescriba ese valor para ampliarlo. Si necesita un margen más largo, existe la opción de declarar SERVICE_ACCEPT_PRESHUTDOWN y recibir SERVICE_CONTROL_PRESHUTDOWN, que se notifica antes que los demás y cuyo tiempo de espera se puede configurar con ChangeServiceConfig2 (el valor predeterminado es de 10 segundos desde Windows 10 Creators Update en adelante, y de 3 minutos en versiones anteriores). Sin embargo, PRESHUTDOWN hace esperar a todo el apagado del sistema durante ese tiempo, así que debe limitarse a los casos realmente necesarios, y en el fondo el propio proceso de detención debería diseñarse para terminar en unos segundos.
- ¿Es seguro hacer el cierre en el apagado usando AppDomain.ProcessExit de .NET?
- Se recomienda no depender de él. Antes, el runtime registraba un manejador de señal predeterminado y el evento ProcessExit se disparaba con CTRL_CLOSE_EVENT o CTRL_SHUTDOWN_EVENT, pero desde .NET 10 el runtime ya no ofrece un manejador de señal de terminación predeterminado, y ProcessExit ya no se dispara en esos casos. Implemente el cierre por la ruta de notificación que corresponda a su modelo de aplicación: en apps GUI, FormClosing o SessionEnding (aunque, al ser una notificación de la fase de consulta, limítelo a un guardado idempotente, y realice el cierre que solo puede hacerse tras la confirmación mediante un hook de WM_ENDSESSION); en Generic Host/Worker Service, IHostApplicationLifetime y StopAsync; y en apps de consola, SetConsoleCtrlHandler o PosixSignalRegistration.
- ¿Cómo se puede evitar que los archivos se dañen ante un corte de energía repentino?
- Como un corte de energía no trae ninguna notificación, la única opción es escribir de forma que «no se rompa sin importar en qué momento se corte». Lo básico es no sobrescribir directamente el archivo original, sino escribir por completo en un archivo temporal de la misma unidad, vaciar el búfer y sustituir con ReplaceFile (File.Replace en .NET). En el funcionamiento normal, esto deja siempre disponible para lectura o bien el archivo antiguo completo o bien el nuevo completo, pero la atomicidad de ReplaceFile ante un corte de energía no está garantizada por especificación, así que hay que conservar una copia de seguridad (el tercer argumento) e implementar también la lectura de arranque: verificar el archivo principal y, si está dañado, recurrir a la copia de seguridad. Además, como el éxito de WriteFile no significa que haya llegado al disco, en los hitos importantes hay que confirmar la escritura con FlushFileBuffers o FILE_FLAG_WRITE_THROUGH. En los PC industriales, lo habitual es combinarlo con un SAI que detecte el paso a alimentación por batería y enlace 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.