Lo spegnimento di Windows visto dalla tua app — Sopravvivere correttamente a notifiche di uscita, riavvii e interruzioni di corrente

· · Windows, Spegnimento, Sviluppo Windows, Servizi Windows, PC di dispositivo, Integrità dei dati, Funzionamento continuo, UPS

«Dopo un riavvio notturno di Windows Update, l’app di misura sul PC di dispositivo è andata giù a metà scrittura, e al mattino il file di misura era corrotto.» «Qualcuno si è disconnesso da un PC condiviso e si è lamentato che le modifiche non salvate erano sparite.» — Per le app Windows a esecuzione lunga, queste due consulenze sono classiche.

Ciò che entrambi i siti hanno in comune è trattare lo spegnimento come «un evento anomalo che non dovrebbe succedere». In realtà, però, dai riavvii automatici di Windows Update, dalla disconnessione dell’utente e da uno spegnimento avviato dall’UPS fino a una perdita di corrente senza preavviso, eventi che tagliano l’esecuzione dall’esterno dell’app arriveranno, prima o poi. Non potete impedirne l’arrivo. Ciò che potete impedire è «perdere i dati quando arrivano».

Per fortuna, Windows ha un meccanismo che notifica l’app prima dello spegnimento, per app GUI, app console e servizi allo stesso modo. Rivolto al personale IT delle piccole e medie imprese e agli sviluppatori di app Windows (soprattutto di PC di dispositivo e app a esecuzione lunga), questo articolo organizza come ricevere quelle notifiche, come progettare una pulizia che «finisce in pochi secondi», il recupero automatico dopo un riavvio e come prepararsi a una perdita di corrente che non porta nessuna notifica — tutto a partire dalle fonti primarie Microsoft Learn ad agosto 2026.

1. Prima di tutto, la conclusione

  • Progettate lo spegnimento come «un evento normale che arriverà, prima o poi». Il tempo che potete usare dopo aver ricevuto la notifica è, in principio, solo circa 5 secondi, quindi un progetto che salva freneticamente tutto sul posto crollerà. Il prerequisito è un salvataggio automatico frequente così che «il delta che deve essere salvato allo spegnimento» resti piccolo.1
  • Sui sistemi client da Windows 8 in poi, quando l’avvio rapido è abilitato (il default sulla maggior parte dei PC che supportano l’ibernazione), «Arresta» è un hybrid shutdown, e il kernel sta solo ibernando. L’unica cosa che viene azzerata del tutto è «Riavvia». Questa è la vera ragione di «l’ho spento e non è migliorato, poi ho riavviato e sì».2
  • Un’app GUI dovrebbe restituire TRUE subito a WM_QUERYENDSESSION, e fare la pulizia in WM_ENDSESSION. In principio non dovete restituire FALSE (rifiutare).1
  • Solo quando avete davvero un’operazione che non si può interrompere dovreste visualizzare un motivo con ShutdownBlockReasonCreate. Anche allora l’utente e l’OS possono forzare la continuazione, quindi un progetto che assume «possiamo bloccare» non regge.34
  • Un’app console riceve la notifica con SetConsoleCtrlHandler. Il periodo di grazia è ancora più breve — un default di 5 secondi per la chiusura della console. C’è anche una trappola: in un processo che ha caricato gdi32.dll o user32.dll, alcuni di questi eventi non arrivano.56
  • La pulizia che si affida ad AppDomain.ProcessExit di .NET, da .NET 10 in poi, non gira sui percorsi in cui il processo è «terminato dall’esterno». Su un’uscita normale come il ritorno da Main gira ancora come prima, ma poiché il runtime non fornisce più la gestione di default per i segnali di terminazione come chiusura della console e spegnimento, la pulizia su quei percorsi deve spostarsi sulla notifica che corrisponde al modello dell’app.7
  • Un servizio Windows può ricevere SERVICE_ACCEPT_PRESHUTDOWN prima, e con un periodo di grazia configurabile, rispetto a SERVICE_ACCEPT_SHUTDOWN (circa 20 secondi di grazia). Il timeout PRESHUTDOWN di default, però, è stato accorciato a 10 secondi da Windows 10 Creators Update in poi, quindi in ogni caso vi serve un progetto che non si appoggi troppo sul periodo di grazia.89
  • Il recupero automatico dopo un riavvio si ottiene combinando RegisterApplicationRestart con ARSO (accesso automatico). Sono previsti percorsi di recupero per crash, non risponde e riavvio guidato da aggiornamento.1011
  • Una perdita di corrente non porta nessuna notifica. Il pattern standard è scrivere completamente su un file temporaneo, fare flush e scambiare con ReplaceFile, ma poiché ReplaceFile non garantisce l’atomicità attraverso una perdita di corrente, un percorso di recupero di backup (.bak) più validazione al caricamento fa parte del set. L’isolamento a posteriori si può fare dal registro eventi (1074/41/6008).121314

In una frase, la conclusione di questo articolo è: «tenete sempre uno stato da cui potete chiudere bottega in pochi secondi quando arriva la notifica, e scrivete in un modo che non si rompe nemmeno su una perdita di corrente che non porta nessuna notifica».

2. Cosa succede allo spegnimento — Quattro modi di finire

2.1. Disconnessione, spegnimento, riavvio e perdita di corrente

Dal punto di vista dell’app, ciò che conta sono due assi: «come finisce la sessione utente» e «cosa succede al kernel».

Operazione Sessione utente Kernel e driver Notifica all’app
Disconnessione Finisce Continua a girare WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION
Spegnimento (avvio rapido abilitato) Finisce Iberna (salvato in hiberfil.sys) WM_QUERYENDSESSION → WM_ENDSESSION, (PRE)SHUTDOWN ai servizi
Riavvio Finisce Finisce del tutto; il prossimo avvio è un avvio completo Come sopra
Perdita di corrente Sparisce subito Sparisce subito Nessuna

Disconnessione e spegnimento sono, dal punto di vista dell’app, quasi lo stesso evento. Se il bit ENDSESSION_LOGOFF è impostato nell’lParam di WM_QUERYENDSESSION è una disconnessione; se è 0 è uno spegnimento o un riavvio (non potete distinguere i due).1 In altre parole, la compiacenza di «è solo una disconnessione, andrà tutto bene» non regge, e il progetto corretto è che venga chiamato lo stesso codice di pulizia.

Quattro modi di finire, e la notifica all'appDisconnessione, spegnimento e riavvio consegnano la notifica WM_QUERYENDSESSION verso WM_ENDSESSION, e la pulizia finisce in pochi secondi. Solo una perdita di corrente non ha nessuna notifica, quindi vi preparate con il progetto di scrittura del capitolo 8 e un UPSDisconnessioneQUERY → ENDSESSIONSpegnimentoRiavvioPerdita di correnteNessuna notifica: scrittura + UPSPulizia in secondi

Figura 1: Disconnessione, spegnimento e riavvio consegnano la notifica WM_QUERYENDSESSION verso WM_ENDSESSION, e la pulizia finisce in pochi secondi. Solo una perdita di corrente non ha nessuna notifica, quindi vi preparate con il progetto di scrittura del capitolo 8 e un UPS.

2.2. La vera ragione di «l’ho spento e non è migliorato» — hybrid shutdown

La riga facile da perdere è la seconda della tabella. Sui sistemi client da Windows 8 in poi, l’avvio rapido (hybrid shutdown) è abilitato per default sui PC che supportano l’ibernazione, e il comportamento di «Arresta» è cambiato. La disconnessione della sessione utente avviene ancora come al solito, ma la sessione kernel non viene chiusa; viene salvata, driver di dispositivo e tutto, nel file di ibernazione (hiberfil.sys) e ripristinata così com’è al successivo avvio. Questo rende l’avvio più veloce, ma lo stato di kernel e driver sopravvive anche dopo che tagliate l’alimentazione.2 Questo è, però, comportamento condizionale. In un ambiente in cui l’ibernazione stessa è disabilitata (powercfg /hibernate off), in cui criteri o Opzioni risparmio energia hanno spento l’avvio rapido, e su Windows Server, lo spegnimento è uno spegnimento completo convenzionale. Potete capire in quale modo sta girando un dato PC dalla casella «Attiva avvio rapido» in Opzioni risparmio energia, o dal fatto che powercfg /a (stati di sospensione disponibili) elenchi «Fast Startup».

Cosa succede al kernel su un'operazione ArrestaUn'operazione Arresta si spezza in uno spegnimento completo o in un'ibernazione del kernel a seconda che l'avvio rapido sia acceso, e Riavvia fa sempre un avvio completoAvvio rapido accesoIbernazione off / ServerArrestaRiavviaSessione finisce + kernel ibernaSpegnimento completoProssimo: ripristina il kernelProssimo: avvio completo

Figura 2: Un’operazione Arresta si spezza in uno spegnimento completo o in un’ibernazione del kernel a seconda che l’avvio rapido sia acceso, e Riavvia fa sempre un avvio completo.

«Riavvia», invece, esegue sempre un ciclo di avvio completo. Dopo un aggiornamento driver, per esempio, vi serve uno stato completamente nuovo.2 Da questo, diversi fenomeni che sentite sul campo trovano posto.

  • «L’ho spento e riacceso, ma il problema del dispositivo non se n’è andato» — kernel e driver sono stati solo ripristinati dall’ibernazione; non sono stati azzerati
  • «È migliorato dopo che ho riavviato» — perché un avvio completo li ha inizializzati
  • Le procedure di incidente sui PC di dispositivo dovrebbero dire «Riavvia», non «spegnilo e accendilo»

Se volete rendere esplicito uno spegnimento completo dalla riga di comando, shutdown /s (il default di Shutdown.exe è uno spegnimento completo); se volete il comportamento hybrid di default, shutdown /s /hybrid.2 Disabilitare l’avvio rapido non è raccomandato. Il lato app dovrebbe assumere «allo spegnimento il kernel può solo stare ibernando» — per esempio, non stimare il «tempo di attività cumulativo» dall’ora di avvio dell’OS — e progettare così da non rompersi in nessuno dei due modi (se l’avvio rapido è acceso o spento differisce per ambiente).

3. Come dovrebbe comportarsi un’app GUI — WM_QUERYENDSESSION e WM_ENDSESSION

3.1. Come i due messaggi spezzano il lavoro

Un’app che ha una finestra e una coda di messaggi viene notificata della fine sessione in due stadi.1

  1. WM_QUERYENDSESSION — una query: «va bene finire?» L’app dovrebbe restituire TRUE subito; la risposta di default di DefWindowProc è anch’essa TRUE. Non iniziate la pulizia qui.
  2. WM_ENDSESSION (wParam=TRUE) — una notifica confermata: «la sessione sta davvero finendo». La pulizia avviene qui.

Restituire FALSE a WM_QUERYENDSESSION può abortire lo spegnimento, ma la documentazione è esplicita: «dovreste restituire TRUE e rispettare l’intento dell’utente», e un’app che ha restituito FALSE viene comunque esposta nell’UI a schermo intero come «un’app che sta impedendo lo spegnimento». Le app console e le app senza finestra visibile non possono abortire lo spegnimento in partenza, e se non rispondono entro 5 secondi vengono terminate automaticamente.14

Flusso della notifica di fine sessione in due stadiRestituire TRUE alla query WM_QUERYENDSESSION conferma con WM_ENDSESSION e gira la pulizia. Rifiutare con FALSE visualizza l'app come una che sta impedendo lo spegnimento, e circa 5 secondi senza risposta possono forzare la continuazioneTRUE(regola)FALSE(rifiuta)Nessuna risposta ~5sForza continuazioneAnnullaWM_QUERYENDSESSIONWM_ENDSESSION(confermato)Mostrata come bloccante lo spegnimentoTrattata come bloccataPulizia quiUscita del processoSpegnimento abortito

Figura 3: Restituire TRUE alla query WM_QUERYENDSESSION conferma con WM_ENDSESSION e gira la pulizia. Rifiutare con FALSE visualizza l’app come una che sta impedendo lo spegnimento, e circa 5 secondi senza risposta possono forzare la continuazione.

3.2. Cosa succede se non rispondete — il muro dei 5 secondi

Sia su WM_QUERYENDSESSION sia su WM_ENDSESSION, potete ritardare la risposta di circa 5 secondi. Oltre, il sistema visualizza la schermata «Questa app sta impedendo lo spegnimento», e l’utente può scegliere di forzare la continuazione (= terminare forzatamente l’app).4 A un processo terminato forzatamente non viene data un’altra occasione per finire il salvataggio.

I punti di progetto sono quindi questi due.

  • Tenete la pulizia a una quantità che finisce entro 5 secondi. Microsoft stessa raccomanda di salvare i dati di frequente in operazione ordinaria così che meno debba essere salvato allo spegnimento, e di salvare i dati non salvati in una posizione temporanea da ripristinare al successivo avvio.1
  • Non mettete su una finestra di conferma durante lo spegnimento. Mentre restate in attesa su «Vuoi salvare?», i 5 secondi passano. Cadete in silenzio sul lato sicuro (salvataggio automatico).

3.3. Implementazione in WinForms e WPF

In un’app desktop .NET, questi messaggi vengono tradotti in eventi del framework. In WinForms, FormClosing viene sollevato, e CloseReason vi dice se lo spegnimento è la causa.

// WinForms: FormClosing is also raised on shutdown / sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // Do only an idempotent snapshot save. Do not show a dialog.
        // Do not set e.Cancel = true (refuse) either.
        SaveWorkingStateToTempFile();
        return;
    }

    // For ordinary closes such as the user clicking the × button, you may confirm here
}

In WPF, l’evento Application.SessionEnding (l’attributo XAML SessionEnding, o un override OnSessionEnding) corrisponde.

Come gli eventi WinForms/WPF si mappano sui messaggiLa fase query di WM_QUERYENDSESSION si mappa su FormClosing di WinForms e SessionEnding di WPF, e ciò che fate lì è al massimo un salvataggio snapshot idempotente. Non c'è un evento corrispondente per WM_ENDSESSION confermato, quindi ricevetelo in WndProc o in un hook e fate la pulizia che può girare solo dopo la confermaWM_QUERYENDSESSIONWinForms: FormClosingWPF: SessionEndingSolo snapshot idempotenteWM_ENDSESSIONNessun evento: hook WndProcPulizia dopo la conferma

Figura 4: La fase query di WM_QUERYENDSESSION si mappa su FormClosing di WinForms e SessionEnding di WPF, e ciò che fate lì è al massimo un salvataggio snapshot idempotente. Non c’è un evento corrispondente per WM_ENDSESSION confermato, quindi ricevetelo in WndProc o in un hook e fate la pulizia che può girare solo dopo la conferma.

// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // You can distinguish ReasonSessionEnding.Logoff / Shutdown,
    // but the baseline is to run the same snapshot save in either case
    SaveWorkingStateToTempFile();

    // Do not set e.Cancel = true unless you have an exceptional reason
}

C’è una cautele qui. Sia FormClosing (CloseReason.WindowsShutDown) sia SessionEnding di WPF corrispondono alla fase query (WM_QUERYENDSESSION). Se un’altra app rifiuta, lo spegnimento viene abortito e la vostra app continua a girare. Quindi ciò che potete fare in questi eventi è un salvataggio snapshot idempotente che non fa danno se lo spegnimento viene abortito e produce lo stesso risultato quante volte gira. Se vi serve «pulizia che deve essere fatta solo quando stiamo davvero finendo» (disconnettere, restituire risorse, e così via), agganciate direttamente il WM_ENDSESSION confermato (wParam=TRUE) in WndProc e fatela lì.

Su entrambi i percorsi, piegate il corpo in una funzione comune di «salvataggio snapshot» e scrivete i dati di ripristino per un’uscita normale, uno spegnimento e (se possibile) un crash nello stesso formato, così che la logica di ripristino al successivo avvio sia un unico percorso. Progettare per lasciare informazioni anche su un crash è trattato in «Progettare app Windows che lascino log e dump quando crashano».

4. Se dovete davvero bloccare — ShutdownBlockReasonCreate

Le operazioni che si rompono fisicamente se vengono tagliate a metà, come scrivere un CD o un firmware, sono l’eccezione. La pratica corretta qui è registrare una stringa di motivo con ShutdownBlockReasonCreate quando l’operazione non interrompibile inizia, e chiamare ShutdownBlockReasonDestroy subito quando finisce. Quando viene richiesto lo spegnimento, quel motivo viene visualizzato sulla schermata «Questa app sta impedendo lo spegnimento», e l’utente può decidere se continuare o annullare.3

Flusso della protezione con ShutdownBlockReasonCreateRegistrate un motivo quando l'operazione non interrompibile inizia; se arriva una richiesta di spegnimento mentre è protetta, il motivo viene mostrato a schermo intero e WM_QUERYENDSESSION viene rifiutato con FALSE. L'utente può annullare o forzare la continuazione, e il motivo viene cancellato quando l'operazione finisceAnnullaForza continuazioneInizia il lavoro non interrompibileShutdownBlockReasonCreateEsegui su un thread workerFine: DestroySpegnimento durante questoMostra motivo + FALSEUscita del processo

Figura 5: Registrate un motivo quando l’operazione non interrompibile inizia; se arriva una richiesta di spegnimento mentre è protetta, il motivo viene mostrato a schermo intero e WM_QUERYENDSESSION viene rifiutato con FALSE. L’utente può annullare o forzare la continuazione, e il motivo viene cancellato quando l’operazione finisce.

[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);

// Call from the thread that created the main window (it fails from other threads)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
    // Run the uninterruptible operation on a worker thread. If you run it
    // synchronously on the UI thread the message pump stops, and the process
    // is force-continued as "Not Responding" before the WM_QUERYENDSESSION
    // refusal code below can run
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// Also, refuse WM_QUERYENDSESSION with FALSE only while protected
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // Refuse. The registered reason string is shown in the full-screen UI
        return;
    }
    base.WndProc(ref m);
}

Il fraintendimento facile qui è la divisione dei ruoli. Tutto ciò che ShutdownBlockReasonCreate fa è registrare una stringa di motivo; non ferma lo spegnimento da sola. Ciò che effettivamente trattiene lo spegnimento è la vostra gestione che restituisce FALSE a WM_QUERYENDSESSION mentre è impostato un flag di protezione, come sopra. Usate i due come un set, e cancellate entrambi subito quando l’operazione finisce. Inoltre, eseguite l’operazione protetta stessa su un thread worker e tenete il thread UI in grado di elaborare i messaggi — il meccanismo di rifiuto funziona solo una volta che il messaggio arriva (e anche allora l’utente e l’OS possono forzare la continuazione, quindi un progetto di scrittura che non si rompe «se non si è fermato» — capitolo 8 — è comunque richiesto).

Ci sono tre cautele operative.

  • Tenete la stringa di motivo breve e specifica. L’utente ha fretta e leggerà solo per pochi secondi. La documentazione stessa dà «Burning a CD» come esempio appropriato.3
  • Non lasciatela registrata per tutta la vita dell’app. «Solo mentre è in corso un’operazione non interrompibile» è ciò che l’API assume.
  • Non progettate sull’assunto che potete bloccare. L’utente può scegliere di forzare la continuazione, e uno spegnimento forzato (ENDSESSION_CRITICAL) non aspetterà in partenza. La documentazione è esplicita: «Applications should not depend on being able to block shutdown».4

5. Come dovrebbero comportarsi app console e processi in background

5.1. SetConsoleCtrlHandler e un periodo di grazia breve

Un’app console non può ricevere messaggi di finestra, quindi i segnali di controllo arrivano a una funzione gestore registrata con SetConsoleCtrlHandler. Il periodo di grazia di default per segnale è il seguente.5

Segnale Quando si verifica Periodo di grazia di default
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break Nessun timeout
CTRL_CLOSE_EVENT Chiusura della console, «Termina attività» del Task Manager (un kill forzato del processo dalla scheda «Dettagli» è un’uscita immediata senza notifica, ed è fuori da questa tabella) Circa 5 secondi
CTRL_SHUTDOWN_EVENT Spegnimento del sistema (processi servizio) Circa 20 secondi

Ci sono due punti da osservare. Primo, essenzialmente solo un processo che gira come servizio può ricevere CTRL_LOGOFF_EVENT e CTRL_SHUTDOWN_EVENT. Un’app in una sessione interattiva viene terminata alla disconnessione, quindi un progetto che aspetta questi segnali non regge.5 Secondo, un processo che ha caricato gdi32.dll o user32.dll viene trattato come un’app Windows anche se lo pensate come un’app console, e i gestori LOGOFF/SHUTDOWN non vengono chiamati. Il workaround ufficiale è creare una finestra nascosta e ricevere WM_QUERYENDSESSION/WM_ENDSESSION.6

Periodo di grazia per segnale consoleCtrl+C e Ctrl+Break non hanno un timeout esplicito; la chiusura della console ha circa 5 secondi e un segnale di spegnimento a un processo servizio ha circa 20 secondi; superarli termina forzatamente il processoNessun timeoutCirca 5sCirca 20sCTRL_C / BREAKPulizia HandlerRoutineCTRL_CLOSECTRL_SHUTDOWNKill forzato dopo la grazia

Figura 6: Ctrl+C e Ctrl+Break non hanno un timeout esplicito; la chiusura della console ha circa 5 secondi e un segnale di spegnimento a un processo servizio ha circa 20 secondi; superarli termina forzatamente il processo.

// Console app: clean up on Ctrl+C and console close
[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;  // Keep a reference so GC does not collect it

static bool OnCtrlEvent(int ctrlType)
{
    // Do only cleanup that finishes within 5 seconds
    FlushAndCloseDataFile();
    return false;   // Proceed to the default handler; the process exits
}

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

5.2. La trappola .NET — non fate affidamento su ProcessExit

In .NET c’è da tempo un pattern di scorta di «basta pulire in AppDomain.ProcessExit», ma da .NET 10 il runtime non fornisce più gestori di default per i segnali di terminazione, e né ProcessExit né AssemblyLoadContext.Unloading scattano su CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT. Il gestore di default dell’OS termina semplicemente il processo subito.7

Come è cambiato ProcessExit in .NET 10Fino a .NET 9 il gestore di segnali di default del runtime riceveva il segnale di terminazione, sollevava ProcessExit, poi usciva. Da .NET 10 il runtime non fornisce un gestore di default, la gestione di default dell'OS termina il processo subito, e registrate un gestore voiCTRL_CLOSE / SHUTDOWNFino a .NET 9: ProcessExitDa .NET 10: uscita immediataRegistrare un gestore voi

Figura 7: Fino a .NET 9 il gestore di segnali di default del runtime riceveva il segnale di terminazione, sollevava ProcessExit, poi usciva. Da .NET 10 il runtime non fornisce un gestore di default, la gestione di default dell’OS termina il processo subito, e registrate un gestore voi.

Invece, spostatevi sul percorso canonico per ciascun modello di app.

  • App GUI: FormClosing / SessionEnding del capitolo precedente
  • Generic Host (incluso Worker Service): IHostApplicationLifetime e BackgroundService.StopAsync. Rendete esplicito il periodo di grazia di stop con HostOptions.ShutdownTimeout
  • App console nuda: SetConsoleCtrlHandler (o iscrivetevi agli equivalenti SIGINT/SIGTERM con PosixSignalRegistration)
Dove ciascun modello di app riceve la notifica di uscitaUn'app GUI usa FormClosing e SessionEnding più un hook WM_ENDSESSION per il lavoro confermato; Generic Host usa IHostApplicationLifetime e StopAsync; un'app console nuda usa SetConsoleCtrlHandler o PosixSignalRegistration. Fare affidamento su ProcessExit non scatta sui percorsi di segnale esternoGUINon GUIHostConsoleQuale modello di app?FormClosing / SessionEndingHost o console?Hook ENDSESSIONLifetime + StopAsyncSetConsoleCtrlHandlerImposta ShutdownTimeoutNon fare affidamento su ProcessExit

Figura 8: Un’app GUI usa FormClosing e SessionEnding più un hook WM_ENDSESSION per il lavoro confermato; Generic Host usa IHostApplicationLifetime e StopAsync; un’app console nuda usa SetConsoleCtrlHandler o PosixSignalRegistration. Fare affidamento su ProcessExit non scatta sui percorsi di segnale esterno.

Il periodo di grazia differisce per percorso — circa 5 secondi per GUI e chiusura console, il periodo di grazia SCM del capitolo 6 per un servizio (circa 20 secondi, o il valore configurato per PRESHUTDOWN), e nessun timeout esplicito per Ctrl+C. Su ogni percorso, però, il periodo di grazia è limitato e non ci si può contare, quindi l’asse di progetto è che il caso normale è «già salvato a ciascun checkpoint di elaborazione», non «lavorare sodo nell’evento di uscita».

6. Come dovrebbe comportarsi un servizio Windows — SHUTDOWN e PRESHUTDOWN

6.1. Due tipi di notifica di spegnimento

Un servizio non è influenzato dalla disconnessione, ma viene fermato allo spegnimento e al riavvio. La notifica arriva come un codice di controllo dal Service Control Manager (SCM), e riceverla richiede di dichiarare un flag di accettazione.8

Dichiarazione Notifica che arriva Tempi e periodo di grazia
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN Notificato durante l’elaborazione dello spegnimento. Default circa 20 secondi, tetto WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN Notificato prima di SHUTDOWN. Lo SCM aspetta finché il servizio si ferma o scade il timeout
Ordine delle notifiche di spegnimento a un servizioQuando inizia lo spegnimento, i servizi che hanno dichiarato PRESHUTDOWN vengono notificati per primi con il periodo di grazia configurato, poi viene inviata la notifica SHUTDOWN con un default di circa 20 secondi, e il processo viene terminato quando scade il periodo di graziaInizia lo spegnimentoPRESHUTDOWN(se dichiarato)SHUTDOWN(circa 20s)Scade la grazia → uscita

Figura 9: Quando inizia lo spegnimento, i servizi che hanno dichiarato PRESHUTDOWN vengono notificati per primi con il periodo di grazia configurato, poi viene inviata la notifica SHUTDOWN con un default di circa 20 secondi, e il processo viene terminato quando scade il periodo di grazia.

Il timeout PRESHUTDOWN si può configurare con ChangeServiceConfig2 (SERVICE_CONFIG_PRESHUTDOWN_INFO); il default è 10 secondi da Windows 10 Creators Update (build 15063) in poi, e 3 minuti prima.9 Se state ancora lavorando dalla vecchia conoscenza che «PRESHUTDOWN vi dà 3 minuti», su un OS attuale avete solo 1/18 del periodo di grazia che vi aspettavate. Inoltre, PRESHUTDOWN trattiene lo spegnimento di tutto il sistema per quell’intervallo, quindi anche la documentazione dice che «should be used only in special circumstances».8

La pratica lato gestore conta altrettanto. Il gestore di controllo deve tornare entro 30 secondi; lasciate il lavoro di stop che richiede tempo a un altro thread, segnalate SERVICE_STOP_PENDING e tornate subito.8

// Win32 service: accept PRESHUTDOWN and leave stop work to a worker
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);   // Tell the worker to stop and return immediately
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// Worker side: if cleanup runs longer than waitHint, keep reporting
// SERVICE_STOP_PENDING periodically while incrementing dwCheckPoint.
// The SCM judges "still alive and making progress" from waitHint and
// checkpoint advance. If reporting stops it can be treated as hung and
// shutdown can proceed. Always report SERVICE_STOPPED when finished

6.2. Un progetto che non si affida al periodo di grazia

Estendere il tetto del periodo di grazia, WaitToKillServiceTimeout, riscrivendolo dal lato servizio è esplicitamente non raccomandato. La documentazione chiede l’opposto — un servizio dovrebbe finire la pulizia il più in fretta possibile così che una macchina alimentata da UPS possa completare lo spegnimento prima che la batteria muoia. La guida è salvare di frequente in operazione ordinaria così che i dati non salvati siano minimi, non spendere tempo a liberare memoria allo spegnimento, e non aspettare troppo a lungo una risposta quando notificate un peer di rete. Inoltre, lo SCM allo spegnimento per default non considera le dipendenze, quindi l’elaborazione di stop deve funzionare ancora «anche se un servizio da cui dipendete è già andato giù».8

Progettare un'elaborazione di stop che non si affida al periodo di graziaSe salvate a ciascun checkpoint di elaborazione così che i dati non salvati siano sempre minimi, la pulizia quando arriva la notifica di stop finisce in pochi secondi. Un progetto che salva tutto all'uscita non starà nel periodo di grazia, e una terminazione forzata perde i datiSalva a ciascun checkpointStop → salva una scheggia → fattoSalva tutto all'uscitaStop → il salvataggio manca la graziaKill forzato → perdita dati

Figura 10: Se salvate a ciascun checkpoint di elaborazione così che i dati non salvati siano sempre minimi, la pulizia quando arriva la notifica di stop finisce in pochi secondi. Un progetto che salva tutto all’uscita non starà nel periodo di grazia, e una terminazione forzata perde i dati.

In un .NET Worker Service (UseWindowsService), SERVICE_CONTROL_STOP e SHUTDOWN vengono tradotti in stop dell’host, e viene chiamato BackgroundService.StopAsync. L’implementazione di scorta al momento della stesura accetta la famiglia STOP/SHUTDOWN; se vi serve anche PRESHUTDOWN avrete bisogno di un gestore esteso. In ogni caso, rendete esplicito HostOptions.ShutdownTimeout e finite StopAsync in pochi secondi. Per costruire un servizio in generale, vedete «Come costruire e operare servizi Windows».

7. Recuperare automaticamente dopo un riavvio

Su un PC di dispositivo o un PC non presidiato, l’ambito di progetto non è solo «sopravvivere allo spegnimento» ma «tornare da soli dopo un riavvio».

7.1. RegisterApplicationRestart e il callback di recupero

Se avete chiamato RegisterApplicationRestart, l’app viene registrata come candidata al riavvio per crash (eccezione non gestita), non risponde, un riavvio dell’app guidato da aggiornamento e un riavvio dell’OS guidato da aggiornamento. Potete registrare argomenti da riga di comando per il riavvio, quindi se includete «quale file era aperto» e «quale punto di ripristino», potete riprendere da dove eravate rimasti dopo il riavvio.10

Le specifiche da interiorizzare sono le seguenti.10

  • La registrazione deve essere finita prima che il problema si verifichi (durante la gestione di WM_QUERYENDSESSION è l’ultima occasione in uno scenario di aggiornamento)
  • Per prevenire un loop di riavvio, un processo che sta girando da meno di 60 secondi non viene riavviato
  • Un processo che gira con privilegi elevati non è candidato al riavvio automatico (il processo non può essere ricreato senza il consenso di elevazione). Il recupero automatico di un’app che ha bisogno di elevazione si progetta tenendo l’UI a privilegi standard e isolando il lavoro privilegiato in un servizio, o con un percorso di avvio esplicito come un’attività dell’Utilità di pianificazione «Esegui con i privilegi più elevati»
  • Il riavvio dopo un crash o un hang passa dal consenso dell’utente; il riavvio dopo un aggiornamento è automatico
  • Per recuperare attraverso un riavvio dell’OS, il lato che richiede il riavvio (un installer e simili) deve chiamare l’API di spegnimento con i flag EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS

Se registrate anche RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) chiama il callback su un crash e vi dà un periodo di grazia per salvare i dati in corso. Se il salvataggio impiega tempo, però, dovete continuare a chiamare ApplicationRecoveryInProgress entro l’intervallo di ping specificato alla registrazione o il lavoro di recupero viene tagliato a metà. Quando il salvataggio è finito, notificate il completamento con ApplicationRecoveryFinished. «Sostituire un file in uso e riavviare» al momento dell’aggiornamento dell’app è territorio di Restart Manager, trattato in dettaglio in «Come sostituire un exe o una DLL che è in uso».

7.2. ARSO — accesso automatico dopo un riavvio da aggiornamento

Dopo un riavvio di Windows Update, se nessuno accede, le app della sessione utente non tornano. Ciò che riempie quel vuoto è ARSO (Winlogon Automatic Restart Sign-On). Quando Windows Update avvia un riavvio, salva in modo sicuro le credenziali dell’ultimo utente interattivo, configura Autologon, e dopo il riavvio accede automaticamente quell’utente e poi blocca lo schermo.11 C’è anche un comando come shutdown /g che richiede un riavvio più il ripresa delle app registrate. Alcuni ambienti lo disabilitano con criteri organizzativi (DisableAutomaticRestartSignOn e simili), quindi quando progettate un recupero non presidiato, controllate questa impostazione come un set. E se state facendo affidamento sull’avvio automatico in una sessione utente per un lavoro in background di cui avete sempre bisogno, la mossa giusta è farne un servizio Windows fin dall’inizio.

Percorso con cui un'app si recupera automaticamente dopo un riavvioSe registrate con RegisterApplicationRestart prima che si verifichi un problema, l'app viene riavviata dopo il consenso dell'utente su un crash o non-risponde, e dopo l'accesso automatico ARSO e il blocco dello schermo su un riavvio guidato da aggiornamento. I processi sotto 60 secondi di runtime e i processi elevati sono fuori ambitoConsensoRegisterApplicationRestartCrash o hangRiavvio da aggiornamentoRiavvio dell'appAccesso ARSO + bloccoNo: sotto 60s / elevato

Figura 11: Se registrate con RegisterApplicationRestart prima che si verifichi un problema, l’app viene riavviata dopo il consenso dell’utente su un crash o non-risponde, e dopo l’accesso automatico ARSO e il blocco dello schermo su un riavvio guidato da aggiornamento. I processi sotto 60 secondi di runtime e i processi elevati sono fuori ambito.

8. Sopravvivere a una perdita di corrente che non porta nessuna notifica — Progetto di scrittura e UPS

8.1. Una scrittura che «non si rompe qualunque sia il momento del taglio» — file temporaneo + ReplaceFile

Un interruttore scattato, un alimentatore fallito o una spina staccata non portano né WM_ENDSESSION né PRESHUTDOWN. Finché «sovrascrivete il file originale in place» per impostazioni o risultati di misura, una perdita di corrente a metà scrittura può lasciare un file rotto che mescola vecchio e nuovo.

Il pattern standard è scrivere completamente su un file temporaneo sullo stesso volume e poi scambiare. ReplaceFile impacchetta la sequenza «salva sul file nuovo → metti da parte l’originale → rinomina → elimina» in una singola API, e porta anche gli attributi del file originale come ora di creazione, ACL e stream alternativi (i tre file devono essere sullo stesso volume).12 File.Replace di .NET chiama questo così com’è.

Flusso di salvataggio e recupero con un file temporaneo e ReplaceFileAl salvataggio, scrivete completamente su un file temporaneo, fate flush e scambiate con ReplaceFile, lasciando i contenuti vecchi in .bak. Al successivo avvio, validate il file primario e cadete su .bak se è rottoAl successivo avvioAl salvataggioIntegroRottoPerdita di corrente a qualsiasi passoValida il primarioUsa così com'èCadi su .bakFlush su discoScrivi un temp completoReplaceFile → .bak

Figura 12: Al salvataggio, scrivete completamente su un file temporaneo, fate flush e scambiate con ReplaceFile, lasciando i contenuti vecchi in .bak. Al successivo avvio, validate il file primario e cadete su .bak se è rotto.

// The standard pattern for settings and data: write completely to a temporary file, then swap, and keep the old contents
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // Create it on the same volume

    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);   // FlushFileBuffers equivalent. Write the OS buffer
                                           // out to disk (device-side cache limits are in 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // Calls ReplaceFile. Keep the old contents as .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // If we fail mid-way, do not leave the temporary file. Repeated failures
        // of a periodic save would fill the volume with complete copies
        try { File.Delete(tmp); } catch { /* Prefer the original exception if delete fails */ }
        throw;
    }
}

Con questo, l’operazione ordinaria vi lascia sempre in grado di leggere o «un file vecchio completo» o «un file nuovo completo». ReplaceFile è, però, un’operazione di namespace a più passi, e l’atomicità attraverso una perdita di corrente non è garantita per specifica. Ecco perché l’esempio sopra tiene un backup (.bak) — il lato lettura valida il file primario all’avvio e cade sul backup se è rotto, come un set. Non potete usarlo per log append-only o CSV, quindi quelli usano un formato che incorpora la rottura, come «una riga = un record, e scarta un’ultima riga rotta in lettura».

8.2. Il successo di WriteFile non è l’arrivo al disco

L’altra premessa è che anche se WriteFile restituisce successo, i dati possono ancora essere solo nella cache OS. Windows mette letture e scritture di file sul buffer di sistema e le riflette su disco periodicamente con lazy writing. Per far arrivare i dati al disco per certo, o fate flush esplicito con FlushFileBuffers, o specificate FILE_FLAG_WRITE_THROUGH a CreateFile così che ogni scrittura attraversi la cache. I metadati del file system sono sempre in cache, quindi confermare i metadati ha bisogno anch’esso di un flush o write-through.13

Chiamare FlushFileBuffers ogni volta è inefficiente, però, e anche la documentazione incoraggia a considerare FILE_FLAG_NO_BUFFERING+WRITE_THROUGH invece di chiamate frequenti.13 In pratica, «flush solo a un checkpoint di transazione o appena prima di chiudere il file» è un compromesso realistico. I meccanismi di questo strato — il cache manager, il lazy writing e il fatto che «ho fatto flush e può ancora non essere arrivato al disco» a causa della cache hardware — sono trattati in profondità in «Cache Manager: quando il vostro WriteFile raggiunge davvero il disco?».

8.3. UPS e monitoraggio della batteria — trasformare una perdita di corrente in uno spegnimento

La vera contromisura contro la perdita di corrente su un PC di dispositivo è un UPS. Pensate al ruolo dell’UPS non come «fermare un blackout» ma come trasformare «una perdita di corrente senza notifica» in «uno spegnimento pianificato con una notifica». Il progetto è un impianto a due stadi.

  1. Progettare il periodo di grazia: tempo di tenuta della batteria UPS > la somma di «rilevare il passaggio a batteria → pulizia di app e servizi → spegnimento OS completo». Se l’elaborazione di stop dei servizi è lenta, questa equazione non regge più (sezione 6.2)
  2. Rilevamento: il passaggio da AC a batteria, e un calo della capacità residua, vengono notificati con l’evento PBT_APMPOWERSTATUSCHANGE. Un’app con una finestra lo riceve come WM_POWERBROADCAST; un servizio senza finestra dichiara SERVICE_ACCEPT_POWEREVENT e lo riceve come SERVICE_CONTROL_POWEREVENT in HandlerEx (WM_POWERBROADCAST non arriva a un gestore di controllo servizio). Alla ricezione, chiamate GetSystemPowerStatus, controllate ACLineStatus (se è su AC) e BatteryLifePercent, e conducete a interrompere la misura, salvare e richiedere lo spegnimento15
Flusso per trasformare una perdita di corrente in uno spegnimento pianificato con un UPSQuando un blackout passa l'UPS a batteria, viene notificato PBT_APMPOWERSTATUSCHANGE, viene controllato lo stato di alimentazione, e salvataggio più una richiesta di spegnimento trasformano una perdita di corrente senza notifica in uno spegnimento pianificato con una notificaBlackoutUPS a batteriaPBT_APMPOWERSTATUSCHANGEGetSystemPowerStatusInterrompi e salvaRichiedi spegnimentoFlusso di notifica usuale(3–6)

Figura 13: Quando un blackout passa l’UPS a batteria, viene notificato PBT_APMPOWERSTATUSCHANGE, viene controllato lo stato di alimentazione, e salvataggio più una richiesta di spegnimento trasformano una perdita di corrente senza notifica in uno spegnimento pianificato con una notifica.

Un UPS USB tipico appare a Windows come una batteria, quindi potete rilevarlo con questa API standard. Se il software di gestione del vendor ha una funzione «spegni l’OS al N% residuo», controllate anche che la soglia si allinei con il tempo di pulizia della vostra app. La ripresa da sospensione o ibernazione, e i problemi a esecuzione lunga, sono un asse separato, trattato in «Sospensione, ibernazione, Modern Standby e app a esecuzione lunga».

9. Come verificare — Provare lo spegnimento in sicurezza

La gestione dello spegnimento tende a diventare «l’abbiamo scritto ma non l’abbiamo mai provato in condizioni equivalenti alla produzione». Tenete una procedura per verificarlo in sicurezza.

  • Provatelo su una macchina di test o una VM: non fatelo prima su un PC di dispositivo di produzione. In un ambiente di test con un checkpoint Hyper-V (snapshot), ripetete spegnimento, riavvio e una perdita di corrente forzata (spegnere la VM). Uno «spegnimento» della VM, però, riproduce solo «l’OS guest che si ferma senza preavviso»; non riproduce la scomparsa della cache volatile di un disco fisico o una rottura dipendente dal controller. Se lo spedite come PC di dispositivo, il controllo finale è un test di taglio di corrente reale su hardware equivalente alla produzione
  • Un controllo rapido con la disconnessione: il percorso WM_QUERYENDSESSION → WM_ENDSESSION gira anche sulla disconnessione (l’unica differenza è che il bit ENDSESSION_LOGOFF è impostato in lParam), quindi potete confermare comodamente il comportamento del codice di pulizia su una macchina di sviluppo1
  • Provate uno spegnimento completo e hybrid separatamente: provate shutdown /s /t 0 (completo), shutdown /s /hybrid /t 0 (comportamento di default) e shutdown /r /t 0 (riavvio) ciascuno2
  • Misurate quanto impiega la pulizia: scrivete un timestamp nel log all’inizio e alla fine della funzione di pulizia, e misurate se sta in 5 secondi (o nel periodo di grazia configurato per un servizio)
Operazioni da verificare e cosa ciascuna può confermareLa disconnessione è un controllo comodo del percorso di notifica; spegnimento completo, hybrid e riavvio da comando confermano il percorso di notifica di produzione e il periodo di grazia; lo spegnimento della VM prova la resilienza allo stop improvviso; un test di taglio di corrente fisico è il controllo finale incluso lo storage fisicoDisconnessionePercorso QUERY → ENDSESSIONshutdown /s /hybrid /rPercorso di produzione + graziaSpegnimento VMStop improvviso del guestTaglio di corrente fisicoStorage incluso(finale)

Figura 14: La disconnessione è un controllo comodo del percorso di notifica; spegnimento completo, hybrid e riavvio da comando confermano il percorso di notifica di produzione e il periodo di grazia; lo spegnimento della VM prova la resilienza allo stop improvviso; un test di taglio di corrente fisico è il controllo finale incluso lo storage fisico.

Per l’isolamento a posteriori, il registro eventi (Sistema) è utile. Su uno spegnimento o riavvio normale, viene registrato l’evento ID 1074 (quale processo ha avviato lo spegnimento, per chi e per quale motivo). Su una perdita di corrente improvvisa o un crash non c’è 1074, e al successivo avvio vengono registrati l’evento ID 41 (Kernel-Power) e 6008 (The previous system shutdown was unexpected).14 «Cosa è successo durante la notte» parte da qui.

# Check the recent history of shutdown-related events
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

Se 1074 mostra «un riavvio da Windows Update» e i dati dell’app erano rotti, il problema è il codice di pulizia. 6008/41 mostrano solo «uno spegnimento inatteso»; vengono registrati anche per una schermata blu (crash) o un reset forzato, non solo per una perdita di corrente. Se il BugcheckCode di 41 è non zero è un crash; se è 0 e non c’è nemmeno un memory dump, una perdita di corrente è probabile — isolate la causa dalle informazioni circostanti, e una volta che sapete che era una perdita di corrente, il progetto di scrittura del capitolo 8 e un UPS sono il passo successivo.

  • Lo spegnimento è «un evento normale che arriverà, prima o poi». Il periodo di grazia dopo la notifica è, in principio, solo circa 5 secondi, quindi il prerequisito è un salvataggio automatico frequente così che «ciò che fate all’uscita» sia minimizzato.
  • Sui sistemi client da Windows 8 in poi, se l’avvio rapido è abilitato, «Arresta» è un hybrid shutdown e il kernel sta solo ibernando. L’unico azzeramento completo è «Riavvia» — scrivete «Riavvia» nella procedura di incidente.
  • Un’app GUI restituisce TRUE subito a WM_QUERYENDSESSION, e fa la pulizia confermata in WM_ENDSESSION. FormClosing e SessionEnding di WinForms/WPF corrispondono alla fase query, quindi ciò che fate lì è al massimo un salvataggio snapshot idempotente. Non mettete su una finestra durante lo spegnimento.
  • Un’operazione che davvero non si può interrompere viene protetta visualizzando un motivo con ShutdownBlockReasonCreate. Non c’è, però, nessuna garanzia da nessuna parte che possiate bloccare.
  • Un’app console riceve la notifica con SetConsoleCtrlHandler; un servizio con SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN. Il periodo di grazia PRESHUTDOWN di default è 10 secondi su un OS attuale. In .NET, smettete di fare affidamento su ProcessExit e spostatevi sul percorso canonico per il modello dell’app.
  • Il recupero dopo un riavvio può essere non presidiato con RegisterApplicationRestart (+ un callback di recupero) e ARSO.
  • Una perdita di corrente non porta nessuna notifica. Preparatevi con uno scambio file temporaneo + ReplaceFile (come un set con backup + validazione al caricamento), un flush ai checkpoint e un UPS che «trasforma una perdita di corrente in uno spegnimento pianificato».
Il quadro complessivo della gestione dello spegnimentoPer una fine con notifica, rispondete con una pulizia che può chiudere bottega in pochi secondi e conducete al recupero automatico dopo un riavvio; per una perdita di corrente senza notifica, preparatevi con una scrittura che non si rompe qualunque sia il momento del taglio e un UPS, e verificate incluso su hardware fisico. Questi due pilastri sono la conclusione dell'articoloCon notificaSenza notificaCome finiscePulizia in pochi secondi(3–6)Scrittura sicura + UPS(8)Recupero automatico dopo riavvio(7)Verificare su hardware(9)

Figura 15: Per una fine con notifica, rispondete con una pulizia che può chiudere bottega in pochi secondi e conducete al recupero automatico dopo un riavvio; per una perdita di corrente senza notifica, preparatevi con una scrittura che non si rompe qualunque sia il momento del taglio e un UPS, e verificate incluso su hardware fisico. Questi due pilastri sono la conclusione dell’articolo.

  • Verificate in sicurezza su una VM e con la disconnessione, e isolate a posteriori con gli eventi ID 1074/41/6008.

La prossima volta che aggiungete una funzione all’app, chiedetevi questo una volta: se WM_ENDSESSION arriva in mezzo a questo lavoro, o viene staccata la spina, cosa resta al successivo avvio? Scrivere quella risposta nel progetto è il percorso più breve per non passare mai una mattina in piedi davanti a un PC di dispositivo con la testa tra le mani.

Articoli correlati

Aree di consulenza correlate

KomuraSoft LLC si occupa di progetto e implementazione di contromisure di spegnimento e perdita di corrente per PC di dispositivo e app a esecuzione lunga, di indagine sulle cause di corruzione dati e incidenti «si era fermato al mattino» che partono da un riavvio di Windows Update o da una disconnessione, e di revisione di progetto dell’elaborazione di stop dei servizi Windows e del recupero automatico. Va bene partire dallo stadio di «ogni volta che spegniamo qualcosa sembra rompersi, e non so da dove cominciare».

Riferimenti

  1. Microsoft Learn, WM_QUERYENDSESSION message. Sul fatto che WM_QUERYENDSESSION viene inviato a fine sessione e l’app dovrebbe restituire TRUE e rispettare l’intento dell’utente (il default di DefWindowProc è anch’esso TRUE); sul fatto che la pulizia dovrebbe essere differita fino a WM_ENDSESSION; sul fatto che dopo 5 secondi il sistema visualizza UI per le app che stanno impedendo lo spegnimento e l’utente può terminare forzatamente; sul significato dei bit ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL in lParam; sul fatto che spegnimento e riavvio non si possono distinguere; e sul fatto che i dati andrebbero salvati di frequente così che meno debba essere salvato all’uscita.  2 3 4 5 6 7

  2. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. Sul fatto che con l’avvio rapido la sessione kernel non viene chiusa e viene trattata come ibernazione, e lo stato di kernel e driver di dispositivo viene salvato in hiberfil.sys; sul fatto che «Riavvia» fa sempre un avvio completo perché serve uno stato Windows completamente nuovo; sul fatto che l’avvio rapido è abilitato per default e disabilitarlo non è raccomandato; e sul fatto che il default di Shutdown.exe è uno spegnimento completo, con l’opzione /hybrid che produce il comportamento hybrid.  2 3 4 5

  3. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). Sul fatto che la si chiama all’inizio di un’operazione non interrompibile per registrare una stringa di motivo e si chiama ShutdownBlockReasonDestroy a fine; sul fatto che si può chiamare solo dal thread che ha creato la finestra; e sul fatto che l’utente leggerà il motivo solo per pochi secondi, quindi la stringa dovrebbe essere breve e chiara.  2 3

  4. Microsoft Learn, Shutdown Changes for Windows Vista. Sul fatto che la risposta a WM_QUERYENDSESSION/WM_ENDSESSION può essere ritardata di 5 secondi ciascuno e l’utente può poi scegliere di continuare o annullare; sul fatto che un’app console o un’app senza finestra visibile non può abortire lo spegnimento e viene terminata automaticamente dopo 5 secondi di nessuna risposta o una risposta FALSE; sul fatto che se serve un blocco andrebbe registrato un motivo con ShutdownBlockReasonCreate; e sul fatto che un’app non deve dipendere dal poter bloccare lo spegnimento.  2 3 4

  5. Microsoft Learn, HandlerRoutine callback function. Gli eventi CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN ricevuti da un gestore registrato con SetConsoleCtrlHandler; sul fatto che il timeout di default per CTRL_CLOSE_EVENT è circa 5000 millisecondi e per CTRL_SHUTDOWN_EVENT su un processo servizio circa 20000 millisecondi; sul fatto che CTRL_LOGOFF/SHUTDOWN_EVENT vengono ricevuti essenzialmente solo dai servizi perché un’app interattiva viene terminata al logoff; e sul fatto che il gestore gira su un thread separato.  2 3

  6. Microsoft Learn, SetConsoleCtrlHandler function. Sul fatto che un processo che ha caricato gdi32.dll o user32.dll viene trattato come un’app Windows e i gestori CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT non vengono chiamati; sul fatto che il workaround è creare una finestra nascosta e gestire WM_QUERYENDSESSION/WM_ENDSESSION; e sul fatto che le funzioni console possono non funzionare correttamente durante la gestione dei segnali.  2

  7. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. Sul fatto che da .NET 10 il runtime non fornisce più un gestore di default per CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT di Windows (gli equivalenti Unix SIGTERM/SIGHUP); sul fatto che la gestione di default dell’OS termina l’app subito e AppDomain.ProcessExit e AssemblyLoadContext.Unloading non scattano più; e sul fatto che la gestione dei segnali appropriata al modello dell’app andrebbe registrata in una libreria di livello superiore o nel codice dell’app.  2

  8. Microsoft Learn, Service Control Handler Function. Sul fatto che un servizio che ha dichiarato SERVICE_ACCEPT_PRESHUTDOWN riceve prima SERVICE_CONTROL_PRESHUTDOWN, poi un servizio SERVICE_ACCEPT_SHUTDOWN riceve SERVICE_CONTROL_SHUTDOWN; sul fatto che il periodo di grazia di default allo spegnimento è circa 20 secondi e il tetto al riavvio OS è WaitToKillServiceTimeout; sul fatto che questo valore non andrebbe esteso; sul fatto che il gestore di controllo dovrebbe tornare entro 30 secondi, segnalare STOP_PENDING e un wait hint, e lasciare il lavoro lungo a un altro thread; sul fatto che la pulizia dovrebbe finire il più in fretta possibile con in mente l’operazione UPS; e sul fatto che lo SCM allo spegnimento per default non considera le dipendenze.  2 3 4 5

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). Sul fatto che dopo la notifica PRESHUTDOWN lo SCM aspetta finché il servizio si ferma o scade il timeout; sul fatto che il timeout di default è 10 secondi da Windows 10 Creators Update (build 15063) in poi e 3 minuti prima; sul fatto che si configura con ChangeServiceConfig2; e sul fatto che lo stato può continuare a essere aggiornato durante SERVICE_STOP_PENDING.  2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). Sul fatto che il riavvio si può registrare per crash, non risponde, aggiornamento e riavvio del computer che accompagna un aggiornamento; sul fatto che si possono specificare argomenti da riga di comando per il riavvio; sul fatto che la registrazione deve essere fatta prima che il problema si verifichi e durante la gestione di WM_QUERYENDSESSION è l’ultima occasione in uno scenario di aggiornamento; sul fatto che un processo sotto 60 secondi di runtime non viene riavviato; sul fatto che il riavvio dopo un crash o un hang passa dal consenso dell’utente; e sul fatto che attraversare un riavvio OS richiede uno spegnimento con EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS.  2 3

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Sul fatto che quando Windows Update avvia un riavvio automatico salva le credenziali dell’ultimo utente interattivo e configura Autologon; sul fatto che dopo il riavvio accede automaticamente l’utente e blocca la sessione; sul fatto che le credenziali salvate vengono eliminate dopo un accesso riuscito; e sul fatto che si può configurare con Criteri di gruppo (DisableAutomaticRestartSignOn e simili).  2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). Sul fatto che ReplaceFile impacchetta in una singola funzione i passi multipli equivalenti a «salva sul file nuovo, rinomina temporaneamente l’originale, rinomina il file nuovo, elimina l’originale»; sul fatto che preserva gli attributi del file originale come ora di creazione, DACL, cifratura, compressione e named stream; e sul fatto che il backup, il file in sostituzione e il file sostitutivo devono essere sullo stesso volume.  2

  13. Microsoft Learn, File Caching. Sul fatto che le scritture vanno sulla cache di sistema per default e vengono riflesse su disco dal lazy writing; sul fatto che FILE_FLAG_WRITE_THROUGH scrive subito su disco; sul fatto che FlushFileBuffers può fare flush esplicito; e sul fatto che i metadati del file system sono sempre in cache, quindi confermare i metadati richiede un flush o write-through.  2 3

  14. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. Sul fatto che un riavvio normale registra l’evento ID 1074 (quale processo ha avviato lo spegnimento, per chi e per quale motivo); sul fatto che un riavvio inatteso registra l’evento ID 41 (Kernel-Power) e 6008 (the previous shutdown was unexpected); e sul fatto che questi ID possono isolare il tipo di riavvio.  2

  15. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. Sul fatto che questo evento viene notificato via WM_POWERBROADCAST su un passaggio tra batteria e AC o un calo della capacità residua; e sul fatto che alla ricezione dovreste chiamare GetSystemPowerStatus e controllare i campi SYSTEM_POWER_STATUS come ACLineStatus, BatteryFlag e BatteryLifePercent. 

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Un problema che non se n'è andato dopo «Arresta» se n'è andato dopo un «Riavvia». Perché?
Sui sistemi client da Windows 8 in poi, quando l'avvio rapido è abilitato (il default sulla maggior parte dei PC che supportano l'ibernazione), «Arresta» usa un meccanismo chiamato hybrid shutdown. L'utente viene disconnesso, ma lo stato del kernel e dei driver viene salvato nel file di ibernazione e ripristinato così com'è al successivo avvio. In altre parole, il nucleo dell'OS non è stato azzerato. «Riavvia», invece, esegue sempre un avvio completo, quindi i problemi di driver e servizi vengono azzerati. Scrivete «Riavvia» nella procedura di isolamento, non «Arresta e riaccendi». Se volete uno spegnimento completo dalla riga di comando, potete usare shutdown /s.
Posso fermare lo spegnimento finché l'app non finisce di salvare?
Potete chiedere di aspettare temporaneamente, ma non potete fermarlo in modo affidabile. Se registrate una stringa di motivo con ShutdownBlockReasonCreate solo mentre è in corso un'operazione non interrompibile, quel motivo compare sulla schermata «Questa app sta impedendo lo spegnimento» e l'utente può decidere se continuare o annullare. L'utente può comunque scegliere di forzare la continuazione, e uno spegnimento forzato o un riavvio guidato da un aggiornamento possono non aspettare affatto. L'approccio corretto quindi non è «bloccare», ma un salvataggio automatico frequente così che meno dati siano a rischio, più una pulizia progettata per finire in pochi secondi dalla notifica di uscita.
Fermare il mio servizio Windows impiega molto tempo. Posso estendere il periodo di grazia dello spegnimento?
Nella configurazione di default che riceve SERVICE_CONTROL_SHUTDOWN, il periodo di grazia è circa 20 secondi e dipende dal valore di registro WaitToKillServiceTimeout. Riscrivere quel valore dal lato app per estenderlo non è raccomandato. Se vi serve un periodo di grazia più lungo, potete dichiarare SERVICE_ACCEPT_PRESHUTDOWN e ricevere SERVICE_CONTROL_PRESHUTDOWN; venite notificati prima degli altri, e il timeout si può configurare con ChangeServiceConfig2 (il default è 10 secondi da Windows 10 Creators Update in poi, e 3 minuti prima). PRESHUTDOWN, però, trattiene l'intero spegnimento per quell'intervallo, quindi limitatelo ai casi di cui avete davvero bisogno, e in fondo progettate il lavoro di stop stesso perché finisca in pochi secondi.
È sicuro fare la pulizia di spegnimento in AppDomain.ProcessExit di .NET?
Raccomando di non farci affidamento. Storicamente il runtime registrava un gestore di segnali di default, e ProcessExit scattava su CTRL_CLOSE_EVENT e CTRL_SHUTDOWN_EVENT, ma da .NET 10 il runtime non fornisce più gestori di default per i segnali di terminazione, e ProcessExit non scatta più in quei casi. Implementate la pulizia sul percorso di notifica che corrisponde al modello dell'app: le app GUI usano FormClosing o SessionEnding (quelle sono notifiche di fase query, quindi limitatele a salvataggi idempotenti; la pulizia che può girare solo dopo che la sessione è confermata appartiene a un hook WM_ENDSESSION); Generic Host / Worker Service usano IHostApplicationLifetime e StopAsync; le app console usano SetConsoleCtrlHandler o PosixSignalRegistration.
Come faccio a evitare che i file vengano corrotti da una perdita di corrente improvvisa?
Una perdita di corrente non porta nessuna notifica, quindi l'unica opzione è scrivere in un modo che non si rompe qualunque sia il momento del taglio. La baseline è non sovrascrivere il file originale in place: scrivete completamente su un file temporaneo sullo stesso volume, fate flush e scambiate con ReplaceFile (File.Replace in .NET). In operazione ordinaria questo vi lascia in grado di leggere o un file vecchio completo o un file nuovo completo, ma l'atomicità di ReplaceFile attraverso una perdita di corrente non è garantita per specifica, quindi tenete un backup (il terzo argomento) e implementate un recupero al caricamento che valida il file primario e cade sul backup se è rotto. Inoltre, il successo di WriteFile non significa che i dati siano arrivati al disco, quindi ai checkpoint importanti confermate la scrittura con FlushFileBuffers o FILE_FLAG_WRITE_THROUGH. Sui PC di dispositivo l'impianto standard è combinarlo con un UPS, rilevare il passaggio a batteria e condurre a uno spegnimento sicuro.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog