Shutdown ב-Windows מנקודת המבט של האפליקציה — לשרוד exit notifications, restart ו-power loss נכון

· עודכן בתאריך: · · Windows, shutdown, פיתוח Windows, Windows service, device PC, שלמות נתונים, long-running, UPS

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 21 Aug 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI (ארכיון רשום): 10.5281/zenodo.22176531)

מזהי ה־DOI שלהלן מפנים לגרסאות שנשמרו בעבר בארכיון, ועשויים שלא להתאים לטקסט הנוכחי. להפניה לטקסט הנוכחי, השתמשו בכתובת של דף זה.

Go Komura (2026). Shutdown ב-Windows מנקודת המבט של האפליקציה — לשרוד exit notifications, restart ו-power loss נכון. KomuraSoft LLC. https://comcomponent.com/he/blog/windows-shutdown-handling-for-apps/

DOI (ארכיון רשום)
10.5281/zenodo.22176531
DOI (הגרסה האחרונה שנרשמה)
10.5281/zenodo.22176532

“Restart לילי של Windows Update השחית את הקובץ שהיינו באמצע למדוד.” “Sign-out ממחשב משותף מוחק מה שערכתי.” כדי למנוע שבירה מהסוג הזה, shutdown אסור להתייחס אליו כחריג: בקשה מבחוץ לצאת חייבת להיות מתוכננת כהתנהגות רגילה של האפליקציה.

קוד שמקבל את ה-exit notification אינו מספיק לבדו. הזמן הזמין אחרי ה-notification קצר, ו-power loss פתאומי לא מביא שום notification. הרעיון המרכזי של המאמר הוא להפוך שמירה שגרתית, cleanup קצר, ו-recovery ב-startup הבא לשלם אחד רציף.

לאנשי IT בעסקים קטנים ובינוניים ולמפתחי אפליקציות Windows, במיוחד מי שעובדים עם device PCs ואפליקציות long-running, המאמר עובר על בחירת נתיב notification, מימושו, recovery אחרי restart, הכנה ל-power loss, ואימות התוצאה. הבסיס הטכני הוא מקורות ראשוניים של Microsoft Learn, נכון לאוגוסט 2026, שהמאמר המקורי הפנה אליהם.

1. קודם כל, המסקנות — לצמצם מה שחייב להישמר לפני שממתינים ל-notification

משאירים את האפליקציה במצב שבו היא יכולה לצאת תוך כמה שניות מ-notification, ומוודאים שה-startup הבא יכול לשחזר גם כשאין notification. זו מדיניות הבסיס לטיפול ב-shutdown.

במקום לשמור כמות גדולה של נתונים אחרי ה-exit notification, שומרים בכל אבן דרך של העבודה ומשאירים את ההפרש שנשאר ביציאה קטן. לאפליקציות GUI ולסגירת console, כ-5 שניות הן הנתון המפתח. ל-services יש grace period אחר, אבל אף אחד מאלה אינו זמן שמובטח לקבל במלואו.123

1.1. לבחור את נתיב ה-notification לאפליקציה שלכם

סוג אפליקציה איפה בקשת היציאה מגיעה הדבר הראשון לעשות נכון סעיף לקריאה
אפליקציית GUI של Win32 WM_QUERYENDSESSION ו-WM_ENDSESSION לענות ל-query ב-TRUE מיד, כעיקרון. לעשות את ה-cleanup אחרי שהיציאה מחויבת ב-WM_ENDSESSION סעיף 3
WinForms / WPF FormClosing / SessionEnding, פלוס message hook במקום שצריך שני ה-events שייכים לשלב ה-query. מעבירים cleanup שאי אפשר לבטל ל-notification המחויב סעיף 3
אפליקציית console רגילה SetConsoleCtrlHandler ודומים סגירה ו-sign-out/shutdown מקבלים notification בתנאים שונים סעיף 5
Windows service SHUTDOWN / PRESHUTDOWN מה-SCM מכריזים על דגל ה-accepted-controls וחוזרים מ-control handler מיד סעיף 6
Generic Host / Worker Service IHostApplicationLifetime ו-StopAsync מרכזים cleanup בנתיב ה-stop של ה-Host ומגדירים ShutdownTimeout במפורש סעיפים 5 ו-6

ב-.NET, אל תסתמכו על AppDomain.ProcessExit לבדו. מ-.NET 10 ואילך, ה-runtime אינו מספק handler ברירת מחדל ל-CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. נתיב האות החיצוני הזה נפרד מיציאה רגילה כמו חזרה מ-Main. סעיף 5.2 מכסה את הפרטים.4

1.2. שתי הכנות נדרשות מחוץ לטיפול ב-notification

כשה-notification מגיע, אל תשאל את המשתמש “האם לשמור?”; סיימו עם cleanup קצר וצאו. חסימה זמנית לעבודה שבאמת אי אפשר להפריע לה מכוסה בסעיף 4, ו-recovery אוטומטי אחרי restart בסעיף 7.

ההכנה האחרת היא להשאיר נתונים שעדיין אפשר לקרוא אחרי power loss שלא מביא notification. כתיבה לקובץ זמני, flush, החלפה, שמירת backup, ואימות ב-startup מכוסים יחד בסעיף 8. אחרי שנתיב ה-notification ממומש, ממשיכים לבדוק את תכנון השמירה וה-recovery בסעיף 8 ואת האימות בסעיף 9.

תכנון שמירה משותף ל-exit notifications ול-power lossשמירה שגרתית משאירה את ההפרש שלא נשמר קטן; עם notification בא cleanup קצר, ובלי אחד הנתונים השמורים מאומתים ומשוחזרים לפני שהעבודה ממשיכהכןלאשמירה בכל אבן דרךיש notification ביציאה?שומרים רק את ההפרש שנשאר ויוצאיםאין cleanup אפשרי במקוםאימות ו-recovery ב-startup הבאהמשך העבודה

איור 1: במקום לעבוד קשה רק כשמגיע notification, הופכים שמירה שגרתית עד ה-startup הבא לתהליך אחד רציף.

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 14, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

2. קודם מבחינים — Sign-out, shutdown, restart ו-power loss

2.1. מסתכלים על session המשתמש ועל ה-kernel בנפרד

פיצול “מה מסתיים” לשני חלקים מקל על ארגון הטיפול. Session המשתמש הוא הטווח שבו אפליקציות המשתמש הזה רצות. ה-kernel וה-drivers, לעומת זאת, שייכים לצד ה-OS וממשיכים לרוץ אחרי שהמשתמש עושה sign-out.

פעולה Session משתמש Kernel ו-drivers Notifications
Sign-out מסתיים ממשיך לרוץ ה-GUI מקבל את query היציאה ואת ה-notification המחויב. Services לא נעצרים ב-sign-out
Shutdown עם Fast Startup דולק מסתיים שומר את מצב ה-hibernation ל-hiberfil.sys Notifications של סיום session ל-GUI ו-notification של shutdown ל-services
Restart מסתיים מסתיים לגמרי ועושה boot מלא בפעם הבאה כמו למעלה
Power loss פתאומי נעלם מיד נעלם מיד אין notification

ב-WM_QUERYENDSESSION של ה-GUI, הסיבית ENDSESSION_LOGOFF ב-lParam מציינת sign-out. אם lParam הוא 0 זה shutdown או restart, ואי אפשר להבחין בין השניים. מתייחסים ל-lParam כמסכת סיביות.1

לנתונים שלא נשמרו של האפליקציה, sign-out ו-shutdown דורשים אותה הכנה. אל תפצלו אותם ל-“אין צורך לשמור ב-sign-out”; מנתבים את שניהם לשגרת שמירה משותפת. זה אינו הופך את תנאי ה-notification לאפליקציות console ול-services לזהים; בודקים את הנתיבים בסעיפים 5 ו-6 לכל אחד.

חושבים על sign-out ועל יציאת מערכת בנפרדSign-out מסיים את אפליקציות המשתמש בזמן ש-services ממשיכים לרוץ; shutdown או restart של המערכת גם עוצרים services; power loss אינו מודיע לאף אחדSign-outאפליקציות המשתמש יוצאותServices ממשיכים לרוץShutdown או restartגם services נעצריםPower lossאין notification לאף אחד

איור 2: Sign-out מאפשר לתרגל את טיפול היציאה של ה-GUI, אבל הוא אינו נחשב כבדיקה של טיפול ה-stop של service.

2.2. למה “Shutdown לא מתקן, אבל Restart כן”

ב-client OS מ-Windows 8 ואילך, Fast Startup דולק כברירת מחדל ברוב המחשבים שתומכים ב-hibernation. בתצורה הזו shutdown עושה sign-out למשתמש, אבל מצב ה-kernel ו-device drivers נשמר לקובץ hibernation ומשוחזר ב-boot הבא. ניתוק החשמל אינו אומר בהכרח שכל מצב ה-OS אופס.5

ההתנהגות הזו מותנית. במקום שבו hibernation מושבת (powercfg /hibernate off), במקום שבו Fast Startup כבוי במדיניות או ב-Power Options, וב-Windows Server, מקבלים את ה-shutdown המלא המסורתי. בודקים את הגדרות Power Options, ומשתמשים ב-powercfg /a כדי לבדוק אם Fast Startup זמין.

Restart, לעומת זאת, תמיד מבצע מחזור boot מלא. בנוהל לבידוד תקלות drivers, כותבים “restart” במפורש ולא “לכבות ולהדליק מחדש”.5

Fast Startup מול boot מלאShutdown עם Fast Startup דולק שומר ומשחזר את מצב ה-kernel וה-drivers, בעוד shutdown מלא או restart מאתחלים הכול ב-boot מלאכןלאShutdownלהשתמש ב-Fast Startup?Hibernate ל-kernel ול-driversשחזור המצב ב-startup הבאShutdown מלאאתחול ב-boot מלא בפעם הבאהRestart

איור 3: ניתוק החשמל אינו זהה לאיפוס ה-kernel וה-drivers.

משורת הפקודה, shutdown /s מבקש shutdown מלא מפורש ו-shutdown /s /hybrid את ההתנהגות ההיברידית. Shutdown.exe כברירת מחדל עושה shutdown מלא, לכן חשוב לא להניח שהוא זהה לפריט “Shut down” על המסך.5

השבתת Fast Startup כפתרון עוקף אינה מומלצת. בונים את האפליקציה כך שתעבוד בין שהיא דולקת ובין שהיא כבויה. למשל, אל תאמדו “זמן הפעלה מצטבר” של מכשיר מזמן ה-boot של ה-OS לבדו; מתחשבים בתכנון בכך שמצב kernel יכול לעבור הלאה.

3. אפליקציות GUI — להפריד את ה-query מהיציאה המחויבת

3.1. WM_QUERYENDSESSION הוא השאלה, WM_ENDSESSION הוא התוצאה

אפליקציה שיש לה window ו-message queue מקבלת הודעה על סיום session בשני שלבים.1

Message משמעות מה לעשות
WM_QUERYENDSESSION השאלה “בסדר לצאת?” מחזירים TRUE מיד, כעיקרון. גם DefWindowProc כברירת מחדל מחזיר TRUE
WM_ENDSESSION, wParam=TRUE סיום ה-session מחויב עושים את ה-cleanup הקצר: שמירה, ניתוק וכן הלאה
WM_ENDSESSION, wParam=FALSE סיום ה-session בוטל האפליקציה ממשיכה לרוץ. אל תעשו cleanup שאפשר רק אחרי שהיציאה מחויבת

החזרת TRUE ל-query בעצמכם אינה אומרת שהיציאה ודאית. אפליקציה אחרת יכולה לסרב, והיציאה מבוטלת. אם מנתקים או מוסרים משאבים שצריך בנקודה הזו, אפליקציה שלא יצאה כבר לא יכולה לעבוד. לכן ה-cleanup עובר ל-notification המחויב.1

ה-query של ה-GUI ותוצאת היציאהגם אחרי החזרת TRUE ל-WM_QUERYENDSESSION אפליקציה אחרת יכולה לבטל את היציאה, כך ש-cleanup אחרי commit רץ רק כש-wParam של WM_ENDSESSION הוא TRUEכןלאWM_QUERYENDSESSIONמחזירים TRUE מיד, כעיקרוןWM_ENDSESSIONהאם wParam הוא TRUE?Cleanup אחרי commitהיציאה בוטלה; ממשיכים לרוץ

איור 4: אל תבלבלו בין התשובה שמתירה את היציאה לבין ה-notification שהיציאה מחויבת.

יש מקרים שבהם אפשר להחזיר FALSE כדי לסרב ליציאה, אבל הכלל הוא לכבד את כוונת המשתמש לצאת. אפליקציה שמסרבת מוצגת כאפליקציה שמונעת shutdown. לאפליקציות console ולאפליקציות בלי window גלוי יש גם אילוצים: בתצורה רגילה, אפליקציה שלא מגיבה תוך 5 שניות יכולה להסתיים אוטומטית. מתייחסים לחסימה כטיפול החריג של סעיף 4 ואל תשתמשו בה לשמירה רגילה.6

3.2. כ-5 שניות אינן ערובה שאפשר לסיים לשמור

אם דוחים את התשובה בכ-5 שניות בשלב WM_QUERYENDSESSION או בשלב WM_ENDSESSION, המערכת מציגה את המסך שמפרט את האפליקציות שמונעות shutdown, והמשתמש יכול לבחור לכפות את ה-shutdown. אחרי סיום כפוי אין הזדמנות לסיים את שאר השמירה.6

הטיפול הוא לשמור באופן שגרתי ולצמצם את ההפרש ביציאה. מחנים מצב עבודה שלא נשמר במיקום זמני ומשחזרים אותו ב-startup הבא. אל תתכננו את האפליקציה להציג דיאלוג אישור בזמן shutdown ולהמתין. מתייחסים לאישור יציאה רגיל ולבקשת יציאה מה-OS כשני דברים נפרדים.1

משאירים את העבודה ביציאה קטנהתכנון ששומר באופן שגרתי משאיר הפרש קטן ביציאה, בעוד תכנון שצובר בזיכרון עד היציאה אינו נכנס ל-grace period הקצר ומסתכן באובדן דרך סיום כפוישמירה שגרתיתההפרש שנשאר ביציאה קטןיציאה עם cleanup קצרצבירה בזיכרון עד היציאהשמירת הכול ביציאהמעט מדי grace period; סיכון לסיום כפוי

איור 5: ההכנה ליציאה תוך כמה שניות קורה לפני ה-exit notification.

3.3. ב-WinForms וב-WPF, מפרידים שמירה מ-cleanup שאי אפשר לבטל

ב-WinForms המקביל הוא FormClosing עם CloseReason.WindowsShutDown; ב-WPF זה Application.SessionEnding. WPF יכול גם לרשום זאת דרך המאפיין SessionEnding ב-XAML, ואפשר לטפל בזה בדריסת OnSessionEnding.

שניהם, עם זאת, הם events בשלב query. מה שמותר כאן הוא לכל היותר “שמירת snapshot idempotent”: חסרת נזק אם היציאה מבוטלת, ומייצרת אותה תוצאה לא משנה כמה פעמים היא רצה. עבודה שאפשר רק אחרי שהיציאה מחויבת, כמו ניתוק, נעשית בקבלת WM_ENDSESSION עם wParam=TRUE ב-WndProc של WinForms או ב-hook של WPF.

חלוקת טיפול היציאה ב-WinForms וב-WPFFormClosing ו-SessionEnding שומרים snapshot שבטוח גם אם היציאה מבוטלת, ו-hook על WM_ENDSESSION עם TRUE מבצע את ה-cleanup שאי אפשר לבטלשלב queryFormClosingSessionEndingשמירת snapshot idempotentWM_ENDSESSION עם TRUEקבלה ב-WndProc או ב-hookעבודה אחרי commit כמו ניתוק

איור 6: אל תדחסו עבודה אחרי commit ל-events של ה-framework.

שתי הדוגמאות הבאות הן החלק ששומר רק את מצב העבודה בשלב ה-query. דוגמאות הקוד הן קטעי מימוש; תוכן פונקציית השמירה, טיפול בכשלים, רישום events וכן הלאה תלויים באפליקציה.

// WinForms: FormClosing נקרא גם ב-shutdown וגם ב-sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // רק שמירת snapshot אידמפוטנטית. בלי דיאלוג.
        // גם e.Cancel = true (סירוב) לא מגדירים.
        SaveWorkingStateToTempFile();
        return;
    }

    // במקרים רגילים, כמו סגירה בלחצן X, מותר לאשר כאן
}
// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // אפשר להבחין בין ReasonSessionEnding.Logoff / Shutdown,
    // אבל הבסיס הוא להריץ את אותה שמירת snapshot לשניהם
    SaveWorkingStateToTempFile();

    // אל תגדירו e.Cancel = true בלי סיבה חזקה מאוד
}

בשני המקרים, מנתבים את הנתונים שבשימוש ביציאה רגילה, ב-shutdown, ואם אפשר גם ב-crash לפונקציית שמירה ופורמט משותפים. אם נמנעים מיצירת פורמט נפרד לכל נתיב שמירה, גם לוגיקת השחזור ב-startup הבא יכולה להיות אחת. תכנון להשאיר מידע ב-crash מכוסה ב-איך משאירים לוגים ו-crash dump כשאפליקציית Windows קורסת.

4. לחסום זמנית, ורק לעבודה שאי אפשר להפריע לה

4.1. רישום סיבה וסירוב לצאת הם עבודות נפרדות

עבודה שנהרסת פיזית אם מפריעים לה, כמו צריבת CD או כתיבת firmware, היא החריג. רושמים סיבה עם ShutdownBlockReasonCreate כשהעבודה מתחילה, ומנקים אותה עם ShutdownBlockReasonDestroy כשהיא מסתיימת. הסיבה הרשומה מוצגת במסך שמפרט את האפליקציות שמונעות shutdown.7

שימו לב, עם זאת, שרישום מחרוזת סיבה לבדה אינו עוצר את ה-shutdown. משלבים אותה עם דגל “מוגן”, ורק כל עוד הוא מוגדר מחזירים FALSE ל-WM_QUERYENDSESSION. כשהעבודה מסתיימת, מנקים גם את הסיבה וגם את דגל ההגנה.

חלוקת תפקידים בחסימת יציאה זמניתרק בזמן שעבודה שאי אפשר להפריע לה רצה, משלבים רישום סיבה עם סירוב ל-query ומנקים את שניהם בסיום; shutdown כפוי בידי המשתמש עדיין אי אפשר למנועביטולכפייהעבודה שאי אפשר להפריע לה מתחילהרושמים את הסיבה ומגדירים את דגל ההגנהעיבוד על workerסיום; מנקים את הסיבה ואת הדגלQuery יציאה מגיע בינתייםמחזירים FALSE ומציגים את הסיבההחלטת המשתמשממשיכים לרוץאפשר לסיים

איור 7: רישום סיבה לבדו אינו סירוב, ואפילו מימוש הסירוב אינו מונע shutdown כפוי.

4.2. משאירים את ה-UI thread מסוגל לקבל את בקשת היציאה

רושמים ומנקים את הסיבה מה-thread שיצר את ה-window היעד. קריאות מ-threads אחרים נכשלות.7 העבודה הארוכה שאי אפשר להפריע לה עצמה, לעומת זאת, עוברת ל-worker thread. אם ה-UI thread חסום בעיבוד סינכרוני, האפליקציה הופכת ל-“Not responding” לפני שה-message שנדרש לסירוב בכלל מעובד.

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

// לקרוא מה-thread שיצר את ה-window הראשי (קריאות מ-threads אחרים נכשלות)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "כותבים נתוני מדידה לקובץ");
try
{
    // את העבודה שאי אפשר להפריע לה מריצים על worker thread. הרצה סינכרונית על
    // ה-UI thread עוצרת את ה-message pump, והאפליקציה נכפית כ-
    // "Not responding" לפני שקוד הסירוב ל-WM_QUERYENDSESSION למטה יכול לרוץ
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// בנוסף, מחזירים FALSE ל-WM_QUERYENDSESSION רק בזמן ההגנה, כדי לסרב
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // סירוב. מחרוזת הסיבה הרשומה מוצגת ב-UI במסך מלא
        return;
    }
    base.WndProc(ref m);
}

הדוגמה הזו היא השלד שמשלב רישום הסיבה, עיבוד על worker, וסירוב ל-query. טיפול בשגיאות, כולל כשלי API, נדרש בנפרד. משאירים את מחרוזת הסיבה קצרה וקונקרטית, ומגבילים את הרישום לזמן שהעבודה באמת אי אפשר להפריע לה במקום להשאיר אותה רשומה לכל חיי האפליקציה.7

גם אז, המשתמש יכול לבחור לכפות את ה-shutdown. יש גם נתיבי יציאה כפויה כמו ENDSESSION_CRITICAL, כך שלעולם אל תהפכו את היכולת לחסום להנחת יסוד של שלמות נתונים. ההכנה למקרה שלא הצלחתם לעצור היא תכנון השמירה וה-recovery של סעיף 8.6

5. אפליקציות console ו-.NET — בודקים את התנאים שבהם notifications מגיעים

5.1. Notifications ו-grace periods שמתקבלים דרך SetConsoleCtrlHandler

באפליקציית console, אותות בקרה מגיעים ל-handler שנרשם עם SetConsoleCtrlHandler. בניגוד לטיפול ב-messages של GUI, ה-handler רץ על thread נפרד.2

Signal מתי הוא מתרחש Grace period כברירת מחדל
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break אין timeout מפורש
CTRL_CLOSE_EVENT סגירת ה-console, “End task” ב-Task Manager וכן הלאה כ-5 שניות
CTRL_SHUTDOWN_EVENT Processes של services ב-shutdown של המערכת כ-20 שניות

סיום כפוי של process מלשונית “Details” של Task Manager וכדומה הוא סיום מיידי בלי notification, והוא מחוץ לטבלה הזו.2

אל תתכננו אפליקציית console ב-session אינטראקטיבי להמתין ל-CTRL_LOGOFF_EVENT או ל-CTRL_SHUTDOWN_EVENT. אפליקציות אינטראקטיביות מסתיימות ברגע ה-sign-out, כך שבפועל רק processes שרצים כ-services יכולים לקבל את אלה. יתר על כן, process שטוען gdi32.dll או user32.dll מטופל כאפליקציית Windows, ו-handlers של LOGOFF/SHUTDOWN אינם נקראים. ה-workaround הרשמי במקרה הזה הוא ליצור window מוסתר ולקבל WM_QUERYENDSESSION / WM_ENDSESSION.28

תנאים לקבלת exit notifications של consoleConsole אינטראקטיבי יכול לקבל את ה-notification של הסגירה אבל אינו יכול לצפות ל-notifications של logoff או shutdown, ואפילו service צריך נתיב notification אחר ברגע שהוא טוען את DLLs של GUISession אינטראקטיביServiceלאכןProcess של consoleסגירה הולכת ל-control handlerממתינים ל-LOGOFF או ל-SHUTDOWN?אי אפשר להסתמך על ה-notification הזהנטענו DLLs של GUI?מטפלים ב-control signal הרלוונטימקבלים דרך window מוסתר

איור 8: אל תתייחסו לטיפול בסגירת console כטיפול ב-shutdown.

הקטע הבא מכין ל-Ctrl+C ולסגירת console. הוא מחזיק ב-delegate כדי שה-GC לא יאסוף אותו, ואחרי cleanup קצר ממשיך ל-handler ברירת המחדל. רישום זה לבדו אינו מבטיח notifications של shutdown לאפליקציה אינטראקטיבית.

// אפליקציית console: cleanup ב-Ctrl+C ובסגירת ה-console
[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;  // מחזיקים הפניה כדי שה-GC לא יאסוף

static bool OnCtrlEvent(int ctrlType)
{
    // רק cleanup שמסתיים תוך 5 שניות
    FlushAndCloseDataFile();
    return false;   // ממשיכים ל-handler ברירת המחדל; ה-process מסתיים
}

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

5.2. מ-.NET 10 ואילך, אל תשימו cleanup רק ב-ProcessExit

מ-.NET 10, ה-runtime כבר לא מספק handler ברירת מחדל ל-CTRL_CLOSE_EVENT / CTRL_SHUTDOWN_EVENT. בלי handler משלכם או מספרייה ברמה גבוהה יותר, עיבוד ברירת המחדל של ה-OS מסיים את ה-process, ובנתיב הזה לא נורים לא AppDomain.ProcessExit ולא AssemblyLoadContext.Unloading.4

זה שינוי להתנהגות ברירת המחדל לאותות סיום חיצוניים. זה אינו אומר ש-ProcessExit כבר לא נורה ביציאה רגילה כמו חזרה מ-Main.

הסרת התלות ב-handler ברירת המחדל לסיום של .NETה-runtime הישן חיבר את האותות הרלוונטיים ל-ProcessExit, אבל מ-.NET 10 אין handler ברירת מחדל, לכן משתמשים בנתיב ה-notification של מודל האפליקציהלאכןאות CLOSE או SHUTDOWNטיפול ברירת המחדל של runtime ישןמעלה ProcessExit ודומיםאין טיפול ברירת מחדל מ-.NET 10 ואילךהאם האפליקציה מטפלת בזה?מסתיים בטיפול ברירת המחדל של ה-OSCleanup שמתאים למודל

איור 9: מבחינים בין יציאה רגילה לסיום באות חיצוני, ומספקים את ה-handler שמודל האפליקציה צריך.

מרכזים cleanup במקום שמתאים למודל האפליקציה. ל-GUI, אלה ה-events וה-notification המחויב של סעיף 3; ל-Generic Host, IHostApplicationLifetime ו-BackgroundService.StopAsync; לאפליקציית console רגילה, SetConsoleCtrlHandler או PosixSignalRegistration. בשימוש באחרון, בוחרים את האותות שמתאימים לנתיבי הסיום שמכוונים אליהם, כמו SIGINT, SIGTERM ו-SIGHUP.4

ב-Generic Host, מגדירים HostOptions.ShutdownTimeout במפורש. ההגדרה בצד ה-Host לבדה, עם זאת, אינה מאריכה את ה-grace period החיצוני של ה-GUI, ה-console או ה-SCM. גם אם ל-Ctrl+C אין timeout מפורש, עדיין צריך להתכונן ל-power loss ולנתיבי סיום אחרים. בכל מודל המדיניות זהה: משאירים את המצב שמור ומשאירים את העבודה אחרי ה-notification קטנה.

עיבוד ה-stop של ה-Host ו-grace period היציאה החיצוניה-stop של Generic Host מרוכז ב-StopAsync, אבל הגדרת ה-timeout של HostOptions אינה מאריכה את grace period היציאה של ה-OS או ה-SCM, לכן בודקים את שניהםבקשת יציאה מה-OS או מה-SCMנתיב ה-stop של ה-HostCleanup ב-StopAsyncמגדירים את זמן ה-stop בצד ה-Hostה-grace period החיצוני קיים בנפרדמודדים אם זה מסתיים מהר

איור 10: בודקים את מגבלות הזמן בצד ה-Host ובצד ה-OS בנפרד, ואל תסתפקו בהארכת ההגדרה.

6. Windows services — חוזרים מ-control handler מיד

6.1. ההבדל בין SHUTDOWN ל-PRESHUTDOWN

Services לא נעצרים ב-sign-out, אבל הם נעצרים ב-shutdown וב-restart. ה-notification מגיע מה-SCM (Service Control Manager), וה-service חייב להכריז על הדגל שמתאים ל-control code שהוא רוצה לקבל.3

דגל להכריז Control code שנמסר מתי להשתמש בו
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN ה-notification הרגיל של shutdown. ה-grace period כברירת מחדל הוא כ-20 שניות ותלוי ב-WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN נמסר לפני ה-SHUTDOWN הרגיל. ה-SCM ממתין עד שה-service נעצר או עד שפג ה-timeout שהוגדר
שלבי exit notification ל-servicesה-SCM מודיע קודם ל-services שמקבלים PRESHUTDOWN, ממתין שהם ייעצרו או שיפוג הזמן, ואז ממשיך ל-notification הרגיל של SHUTDOWNShutdown המערכת מתחילמודיעים ל-services שמקבלים PRESHUTDOWNממתינים ל-stop או למועד שהוגדרמודיעים ל-services שמקבלים SHUTDOWNאחרי ה-grace period, ממשיכים ליציאת המערכת

איור 11: לקבל notification מוקדם ולהיות ממתינים להם הם תנאים נפרדים; גם ל-PRESHUTDOWN יש מועד שהוגדר.

ה-timeout של PRESHUTDOWN מוגדר עם ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO). ברירת המחדל היא 10 שניות מ-Windows 10 Creators Update (build 15063) ואילך ו-3 דקות לפני כן. אם מניחים ש-“PRESHUTDOWN תמיד קונה 3 דקות”, לא תקבלו את הזמן שציפיתם לו.9

כי PRESHUTDOWN מעכב את ה-shutdown של כל המערכת, משתמשים בו רק כשבאמת נחוץ. גם שכתוב WaitToKillServiceTimeout הרגיל מצד ה-service כדי להאריך אותו אינו מומלץ.3

6.2. מפרידים קבלת ה-notification מעצירה בפועל

ה-control handler חייב לחזור תוך 30 שניות, אבל במקום לחשוב על זה כ-30 שניות שאפשר להשתמש בהן, מבנים אותו כך שיסמן את ה-stop ויחזור מיד. מוסרים את העבודה הארוכה ל-thread אחר ומדווחים SERVICE_STOP_PENDING.3

// Win32 service: מקבלים PRESHUTDOWN ומשאירים את עבודת ה-stop ל-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);   // מסמנים ל-worker לעצור וחוזרים מיד
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// צד ה-worker: אם ה-cleanup נמשך יותר מ-wait hint, ממשיכים לדווח
// SERVICE_STOP_PENDING מעת לעת תוך הגדלת dwCheckPoint. ה-SCM שופט לפי
// ה-wait hint וה-checkpoint שמתקדם שה-service "עדיין חי ומתקדם". אם הדיווחים
// נעצרים, מתייחסים אליו כ-hung וה-shutdown יכול להמשיך. תמיד מדווחים SERVICE_STOPPED בסיום

בצד ה-worker, אם ה-cleanup לוקח יותר מ-wait hint, ממשיכים לדווח SERVICE_STOP_PENDING תוך קידום dwCheckPoint. אם דיווחי ההתקדמות נעצרים, ה-service יכול להישפט כ-hung. דיווח SERVICE_STOPPED בסיום הוא חלק מעיבוד ה-stop.39

חלוקת עבודה בין control handler של ה-service ל-workerה-control handler מדווח stop pending, מסמן ל-worker, וחוזר מיד; ה-worker עושה את ה-cleanup ואת דיווח ההתקדמות ולבסוף מדווח stoppedControl handlerדיווח STOP_PENDINGסימון ל-worker לעצורה-handler חוזר מידה-worker מנקהדיווח התקדמות אם זה לוקח זמןדיווח STOPPED בסיום

איור 12: אל תחסמו את ה-thread שמקבל את ה-notification בעיבוד stop ארוך.

6.3. להיות מסוגלים לסיים מהר גם אם תלות נעצרת קודם

ב-shutdown, ה-SCM כברירת מחדל שולח notifications בלי להתחשב בתלויות בין services. עיבוד stop חייב לטפל במקרה שבו service תלות כבר אינו זמין, ואל ימתין יותר מדי לתשובות מצדדים ברשת. במקום לבזבז זמן על שחרור זיכרון וכדומה, נותנים עדיפות לחיוב הנתונים הנחוצים במהירות.3

המדיניות הזו חשובה גם ביחס ל-UPS. ככל ש-service מאריך את ההמתנה, כך קשה יותר להשלים את ה-shutdown של כל ה-OS לפני שהסוללה נגמרת. במקום להגדיל את ה-grace period, מצמצמים את העבודה בזמן stop בשמירה בכל אבן דרך.3

כש-.NET Worker Service רץ תחת UseWindowsService, STOP / SHUTDOWN מומרים ל-stop של Host, שמוביל ל-BackgroundService.StopAsync. המימוש הסטנדרטי, נכון לכתיבת המאמר המקורי, אינו מקבל PRESHUTDOWN, כך שאם צריך אותו צריך להרחיב את מימוש ה-handler. מדיניות הגדרת HostOptions.ShutdownTimeout במפורש וסיום StopAsync עצמו תוך כמה שניות זהה. למימוש הכולל, ראו איך בונים ומפעילים Windows service.

7. Recovery אחרי restart — ליישר רישום, שחזור נתונים ו-sign-in

7.1. RegisterApplicationRestart רושם מראש את נתיב ה-recovery

ב-device PCs ובתפעול unattended, התכנון חורג משמירה ויציאה: הוא מכסה חידוש עבודה אחרי ה-restart. RegisterApplicationRestart הוא ה-API שרושם את האפליקציה ל-restart בכל אחד מהמצבים האלה: crash (חריגה שלא טופלה), not responding, restart של אפליקציה שנגרם מ-update, ו-restart של OS שנגרם מ-update.10

אפשר לציין ארגומנטים של שורת פקודה ל-restart, כמו הקבצים שהיו פתוחים או נקודת שחזור. רישום לבדו, עם זאת, אינו אומר recovery אוטומטי מכל מצב.

פריט לבדיקה תנאי או אילוץ
מתי לרשום לפני שבעיה מתרחשת. בתרחיש update, בזמן טיפול ב-WM_QUERYENDSESSION היא ההזדמנות האחרונה
מניעת לולאת restart Process שרץ פחות מ-60 שניות אינו מופעל מחדש
Crash או hang מופעל מחדש אחרי שהמשתמש מסכים. Restart שנגרם מ-update הוא אוטומטי
מעבר על restart של OS הצד היוזם חייב לקרוא ל-API של shutdown עם EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS
Process elevated אינו זכאי ל-restart אוטומטי, לכן נדרש נתיב הפעלה מפורש נפרד

לאפליקציה שצריכה elevation, מתכננים את נתיב ה-recovery בהרצת ה-UI בהרשאות רגילות והפרדת העבודה המועדפת ל-service, או בשימוש במשימת Task Scheduler שמוגדרת ל-“Run with highest privileges” או דומה.10

7.2. Recovery callback ו-ARSO ממלאים תפקידים שונים

אם משתמשים גם ב-RegisterApplicationRecoveryCallback, WER (Windows Error Reporting) קורא ל-recovery callback ב-crash ואפשר לשמור את הנתונים שעובדים עליהם. בזמן שהשמירה נמשכת, קוראים ל-ApplicationRecoveryInProgress בתוך מרווח ה-ping הרשום, ומודיעים ApplicationRecoveryFinished בסיום. אם דיווחי ההתקדמות נעצרים, עיבוד ה-recovery יכול להיחתך.

שמירה ודיווח התקדמות ב-recovery callbackה-recovery callback ש-WER מפעיל ממשיך לקרוא ל-ApplicationRecoveryInProgress בתוך מרווח ה-ping בזמן שמירה, ומודיע ApplicationRecoveryFinished בסיוםעדיין לאסיוםרישום מראש של recovery callbackCrash; WER מפעיל אותושמירת הנתונים שעובדים עליהםדיווח התקדמות בתוך מרווח ה-pingהשמירה הסתיימה?הודעה ש-recovery הסתייםאפשר לחתוך אם הדיווחים נעצרים

איור 13: אל תשמרו בלבד; מודיעים ל-WER על התקדמות וסיום.

החלפת קבצים שבשימוש בזמן update והפעלה מחדש היא העבודה של Restart Manager. היא מכוסה ב-איך מחליפים EXE או DLL שבשימוש.

המנגנון שמחזיר את session המשתמש אחרי restart של OS, לעומת זאת, הוא ARSO (Winlogon automatic restart sign-on). כש-Windows Update מתחיל restart אוטומטי, הוא שומר בבטחה את ה-credentials של המשתמש האינטראקטיבי האחרון ומגדיר Autologon, ואחרי ה-restart הוא עושה sign-in למשתמש הזה ונועל את המסך.11

רישום restart, sign-in ושחזור נתוניםמול רישום restart מראש, crash דורש הסכמת המשתמש, ואפליקציות משתמש אחרי restart של OS צריכות שחזור session דרך ARSO או דומה ושחזור המצב השמוררישום ל-restart לפני שבעיה מתרחשתCrash או not respondingקבלת הסכמת המשתמשRestart האפליקציהRestart של OS עם הדגלים הנדרשיםשחזור ה-session דרך ARSO או דומהקריאת נקודת השחזור השמורהבדיקת המדיניות ותנאי ה-startup

איור 14: מספקים לא רק את רישום ה-restart של האפליקציה אלא גם את הנתיב שבו ה-session ומצב העבודה חוזרים.

shutdown /g היא הפקודה שמבקשת restart פלוס חידוש אפליקציות רשומות. ARSO יכול להיות מושבת במדיניות ארגונית כמו DisableAutomaticRestartSignOn, לכן בודקים אותו יחד עם דרישות recovery unattended. עיבוד רקע שתמיד נחוץ עדיף להריץ כ-Windows service מאשר לתלות אותו ב-sign-in אוטומטי של המשתמש.11

8. Power loss בלי notification — מתכננים שמירה וטעינה כזוג

8.1. קובץ זמני, החלפה, backup ואימות ב-startup כסט אחד

מפסק שהופעל, ספק כוח שנכשל, או תקע שנשלף לא מביאים לא WM_ENDSESSION ולא PRESHUTDOWN. אם דורסים את הקובץ המקורי במקום, הפרעה באמצע יכולה להשאיר קובץ עם תוכן ישן וחדש מעורבבים.

הבסיס הוא לכתוב הכול לקובץ זמני על אותו volume, לעשות flush, ואז להחליף. ReplaceFile אוגד את הצעדים שמתאימים לשמירה לקובץ חדש, הזחת המקור הצידה, שינוי שם ומחיקה, והוא מעביר הלאה מאפיינים כמו זמן יצירה, ACL ו-alternate data streams. הקובץ שהוחלף, קובץ ההחלפה וה-backup חייבים להיות על אותו volume. File.Replace של .NET קורא ל-API הזה.12

שמירת קובץ ו-recovery ב-startup הבאכותבים לקובץ זמני על אותו volume, עושים flush, מחליפים תוך שמירת backup, ואז מאמתים את הקובץ הראשי ב-startup ונופלים ל-backup אם צריךכןלאקובץ זמני על אותו volumeכתיבה עד הסוף ו-flushהחלפה; התוכן הישן הולך ל-.bakStartup הבאהאם הקובץ הראשי שלם?קוראים את הקובץ הראשינופלים ל-backup

איור 15: מממשים לא רק את הדרך הבטוחה לכתוב אלא גם את הדרך לקרוא כשהקובץ שבור.

// דפוס סטנדרטי לשמירת הגדרות ונתונים: כותבים לקובץ זמני, ואז מחליפים, ומשאירים את התוכן הישן
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // יוצרים על אותו 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. כותב את מאגרי ה-OS
                                           // לדיסק (מגבלות ה-cache בצד ההתקן בסעיף 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // קורא ל-ReplaceFile. משאיר את התוכן הישן כ-.bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // אם נכשל באמצע, לא משאירים את הקובץ הזמני. אם שמירות מחזוריות ממשיכות
        // להיכשל, עותקים שלמים ימלאו בהדרגה את ה-volume
        try { File.Delete(tmp); } catch { /* כישלון מחיקה נסוג מפני החריגה המקורית */ }
        throw;
    }
}

אף על פי שהדוגמה הזו נקראת SaveAtomically, היא אינה מבטיחה atomicity מעבר ל-power loss. בפעולה רגילה היא משאירה קובץ ישן שלם או קובץ חדש שלם לקריאה, אבל ReplaceFile הוא פעולת namespace רב-שלבית, וה-atomicity שלה מול power loss פתאומי אינה מובטחת במפרט. בדיוק לכן שומרים את ה-.bak ומממשים את שגרת הטעינה שמאמתת את הקובץ הראשי ב-startup ונופלת ל-backup אם הוא שבור.12

ה-catch בדוגמה קיים כדי שקבצים זמניים לא יצטברו בכשלים רגילים וימלאו את ה-volume. הוא אינו מצפה שהקוד הזה ירוץ ברגע של power loss. ללוגים ול-CSV של append-only, אל תחילו את גישת החלפת הקובץ כולו כמו שהיא; משתמשים בפורמט שמתחשב באיך הוא נשבר, כמו “כותבים שורה אחת לרשומה וזורקים שורה אחרונה מושחתת בטעינה”.

8.2. מבחינים בין הצלחת WriteFile להגעה לדיסק

גם כש-WriteFile מצליח, הנתונים עדיין יכולים לשבת ב-cache של ה-OS. Windows בדרך כלל כותב למאגרי המערכת ומחיל את הנתונים לדיסק בכתיבה עצלה. ב-checkpoints חשובים, עושים flush עם FlushFileBuffers, או מציינים FILE_FLAG_WRITE_THROUGH בזמן CreateFile כדי לבקש כתיבה מיידית. גם מטא-נתונים של מערכת הקבצים ב-cache, כך שחיובם כרוך ב-flush או write-through.13

Write-through, עם זאת, אינו זהה ל-“לא משתמשים ב-cache של ה-OS”. קריאות FlushFileBuffers תכופות אינן יעילות, לכן שוקלים לשלב עם FILE_FLAG_NO_BUFFERING במקום שצריך. בפועל, התכנון הריאליסטי הוא לעשות flush בנקודות שחשובות לשלמות, כמו גבולות טרנזקציה וממש לפני סגירת הקובץ.13

הגבול בין הצלחת כתיבה להתמדהכתיבה רגילה מגיעה לאחסון מ-cache של ה-OS בהשהיה; flush או write-through דוחפים אותה לחיוב, אבל אילוצי ה-cache בצד המכשיר נשאריםWriteFile מצליחיכול להיות ב-cache של ה-OSכתיבה עצלהFlush ב-checkpointמוחל על האחסוןמגבלות ה-cache בצד המכשיר

איור 16: אל תתייחסו להצלחת API, לחיוב ה-cache של ה-OS, ולעמידות ל-power loss כאותו דבר.

גם ל-cache הנדיף בצד המכשיר יש מגבלות, ואי אפשר לומר “עשינו flush, אז זה שורד לגמרי power loss על כל חומרה”. הקשר בין cache manager, כתיבה עצלה ו-caches של חומרה מוסבר בפירוט ב-Cache Manager: מתי WriteFile מגיע לדיסק?.

8.3. משתמשים ב-UPS כדי להפוך power loss ל-shutdown מתוכנן

תפקיד ה-UPS אינו לבטל הפסקות אלא להפוך power loss בלי notification ל-shutdown מתוכנן עם notification. התנאי שצריך הוא היחס הזה:

זמן ריצת סוללת UPS > זמן לזיהוי המעבר + cleanup של אפליקציות ו-services + השלמת shutdown של ה-OS

המעבר בין חשמל AC לסוללה, ומטען נותר נמוך, מדווחים דרך PBT_APMPOWERSTATUSCHANGE. GUI מקבל זאת דרך WM_POWERBROADCAST; service מכריז SERVICE_ACCEPT_POWEREVENT ואז מקבל SERVICE_CONTROL_POWEREVENT ב-HandlerEx. WM_POWERBROADCAST אינו נמסר ל-control handler של service.14

אחרי הקבלה, בודקים ACLineStatus ו-BatteryLifePercent עם GetSystemPowerStatus, וממשיכים להשהיית המדידה, שמירה ובקשת ה-shutdown.14

מזיהוי UPS עד השלמת shutdownמזהים את מעבר ה-UPS לסוללה דרך נתיב ה-notification המתאים, בודקים את מצב החשמל, ממשיכים לשמירה ול-shutdown, ומתכננים את כל הרצף להיכנס לזמן ריצת הסוללההפסקה; ה-UPS עובר לסוללהPower notification בנתיב המתאיםבדיקת מצב החשמל והמטען הנותרהשהיית המדידה ושמירהבקשת shutdown מה-OSהשלמת ה-cleanup ויציאת ה-OSלהכניס את כל הרצף לזמן הריצה

איור 17: מכניסים לא רק את האפליקציה אלא את הזמן עד יציאת ה-OS לזמן ריצת ה-UPS.

בתצורות שבהן Windows רואה את ה-UPS כסוללה, כמו UPS טיפוסי שמחובר ב-USB, ה-APIs הסטנדרטיים יכולים לנטר אותו. אם לתוכנת הניהול של הספק יש תכונת “shutdown ב-N% נותרים”, בודקים גם שהסף שלה עקבי עם זמן ה-cleanup. חזרה מ-sleep או hibernation היא נושא נפרד; ראו Sleep, hibernation, Modern Standby ואפליקציות long-running.

9. אימות — מאשרים notifications, תזמון ותוצאות recovery

9.1. משחזרים את נתיבי היציאה במחשב בדיקה, לא בפרודקשן

אל תנסו דברים קודם על device PC של פרודקשן; משתמשים במחשב בדיקה או במכונה וירטואלית כמו Hyper-V. במכונה וירטואלית, לוקחים checkpoint מראש וחוזרים על הבדיקות עם נתוני בדיקה שאפשר להרשות לעצמכם לאבד.

פעולה לנסות מה לאשר
Sign-out WM_QUERYENDSESSION → WM_ENDSESSION של ה-GUI ועיבוד השמירה. אינו תחליף לאימות stop של service
shutdown /s /t 0 התנהגות ב-shutdown מלא
shutdown /s /hybrid /t 0 ההתנהגות ההיברידית בתצורה שמשתמשת ב-Fast Startup
shutdown /r /t 0 Restart עם boot מלא, וה-recovery אחריו
כיבוי ה-VM האם ה-startup הבא משחזר גם כש-guest OS נעצר בלי notification
Power loss על חומרה שקולה לפרודקשן עמידות כולל האחסון הפיזי והבקר

Sign-out שונה בכך שסיבית ENDSESSION_LOGOFF מוגדרת, אבל זו דרך קלה לאשר את נתיב ה-notification של ה-GUI. אל תבלבלו בין פקודות full, hybrid ו-restart; מנסים אותן בנפרד.15

הרחבת אימות shutdown בשלביםמנסים את notifications של ה-GUI וכל פעולת יציאה בסביבת הבדיקה, מאשרים recovery אחרי עצירה פתאומית ב-VM, ואז מאמתים עמידות ל-power loss כולל אחסון על חומרה שקולה לפרודקשןהכנת מחשב הבדיקה והנתוניםניסיון נתיבי notification ופעולות יציאהמדידת זמן ה-cleanupאישור recovery אחרי עצירת VM פתאומיתאימות על חומרה אמיתית כולל אחסוןאישור הנתונים וה-recovery ב-startup הבא

איור 18: בודקים לא רק אם האפליקציה יוצאת כרגיל אלא גם מה היא יכולה לשחזר אחרי עצירה פתאומית.

כיבוי VM משחזר רק את עצירת ה-guest בלי אזהרה. הוא אינו יכול לשחזר אובדן cache נדיף של דיסק פיזי או השחתה שתלויה בבקר, לכן כששולחים כ-device PC, עושים את האישור הסופי על חומרה שקולה לפרודקשן.

רושמים חותמת זמן בתחילת ובסוף פונקציית ה-cleanup, ומודדים אם זה נכנס לכ-5 שניות ל-GUI וכדומה, או ל-grace period שהוגדר ל-service. מאשרים לא רק שהיציאה הצליחה אלא גם מה נטען ב-startup הבא ומאיפה העבודה יכלה להתחדש.

9.2. מבודדים מה קרה בלילה מ-Event Log

ב-Windows System log, Event ID 1074 רושם את ה-process שיזם את ה-shutdown, את המשתמש ואת הסיבה. ל-shutdown בלתי צפוי, 41 (Kernel-Power) או 6008 נרשמים ב-startup הבא.15

# בודקים את ההיסטוריה האחרונה של אירועי shutdown
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

אם 1074 מראה restart של Windows Update והנתונים עדיין הושחתו, חוקרים קודם את טיפול ה-exit notification ואת נתיב השמירה. מצד שני, אל תסיקו power loss מ-41 או 6008 לבדם. הם מציינים סיום בלתי צפוי, וגם מסך כחול או reset כפוי הם מועמדים.15

בחירת מה לחקור מ-event log של shutdownמאשרים את ה-process היוזם ואת הסיבה מ-1074, ומתייחסים ל-41 ול-6008 כרמזים לסיום בלתי צפוי להצליב עם מידע מסביב כמו bug check code ו-dumpsSystem event log1074: process יוזם וסיבהחקירת טיפול ה-notification ונתיב השמירה41 ו-6008: סיום בלתי צפויבדיקת BugcheckCode ו-dumpsבידוד crash, power loss וכן הלאה

איור 19: 41 ו-6008 אינם הוכחה ל-power loss עצמו; הם נקודת הכניסה לחקירה נוספת.

BugcheckCode שאינו אפס ב-event 41 הוא רמז ל-crash. אם הוא 0 ואין memory dump, power loss חשוד, אבל מחליטים בהצלבה עם המידע מסביב. אם מתברר שזה power loss, מתמקדים בתכנון השמירה וב-UPS של סעיף 8; אם זו הייתה יציאה מתוכננת, מתמקדים ב-notifications וב-cleanup של סעיפים 3 עד 6.

10. סיכום — מתכננים עד ה-startup הבא, לא רק את טיפול היציאה

טיפול ב-shutdown אינו event handler שרץ פעם אחת ביציאה. מה שחשוב הוא להפוך שמירה שגרתית → cleanup קצר → אימות ו-recovery ב-startup הבא לשלם אחד רציף.

איפה לסקור נקודת תכנון
עיבוד שגרתי שומרים לעיתים קרובות ומצמצמים את ההפרש שנשאר ביציאה
Exit notifications של GUI עונים ל-query ב-TRUE מיד, כעיקרון. מפרידים את השמירה ה-idempotent מה-cleanup אחרי commit
אפליקציות console ו-services משתמשים ב-notification שמתאים למודל האפליקציה. אל תסתמכו רק על ProcessExit של .NET או על הארכת ה-grace period
עבודה שאי אפשר להפריע לה משלבים רישום סיבה וסירוב רק כל עוד נחוץ. מתכוננים גם ל-shutdown כפוי
שמירה וטעינה בנוסף לקובץ הזמני, flush והחלפה, מספקים backup ואימות ב-startup
אחרי restart בודקים את רישום ה-restart, את נתוני השחזור, ואת נתיב ה-sign-in או ה-startup של ה-service

ב-shutdown עם Fast Startup דולק, ה-kernel וה-drivers יכולים לחזור מ-hibernation. בבידוד תקלות, מציינים “restart” במפורש, וגורמים לאפליקציה לעבוד גם תחת shutdown מלא וגם תחת היברידי.5

בדיקה סופית מטיפול יציאה עד recoveryמשאירים מצב שמור במהלך עיבוד רגיל, עושים cleanup קטן ב-exit notification, ומאמתים את הנתונים שנשארים גם בלי notification כדי לשחזר ב-startup הבאכןלאמשאירים מצב שמור באופן שגרתייש exit notification?יוצאים עם cleanup קצרהמצב השמור האחרון הוא כל מה שישאימות ו-recovery ב-startup הבאחוזרים לעבודה בתנאים הנדרשים

איור 20: אל תפרידו את מימוש ה-exit event משמירה שגרתית ו-recovery ב-startup הבא.

לבסוף, מנסים את נתיבי ה-notification ואת הזמן הנדרש במחשב בדיקה, ומאשרים גם את ה-recovery אחרי עצירה פתאומית. בחקירה בדיעבד, משתמשים ב-1074 / 41 / 6008 כרמזים לבידוד יציאה מתוכננת מסיום בלתי צפוי.

בפעם הבאה שמוסיפים תכונה, שואלים “אם exit notification מגיע במהלך העיבוד הזה, או ששולפים את התקע, מה יישאר ב-startup הבא?” הכללת התשובה הזו בתכנון היא מה שמגן עליכם מלגלות השחתת נתונים רק בבוקר שלמחרת.

מאמרים קשורים

תחומי ייעוץ קשורים

KomuraSoft LLC מטפלת בתכנון ובמימוש של אמצעי נגד shutdown ו-power loss ל-device PCs ולאפליקציות long-running, בחקירת שורש של השחתת נתונים וכישלונות “זה היה למטה בבוקר” שמתחילים מ-restart של Windows Update או מ-sign-out, ובסקירות תכנון של עיבוד stop של Windows service ו-recovery אוטומטי. אפשר להתחיל משלב של “משהו נשבר בכל פעם שעושים shutdown, אבל אני לא יודע מאיפה להתחיל”.

קישורים

  1. Microsoft Learn, WM_QUERYENDSESSION message. על כך ש-WM_QUERYENDSESSION נשלח כש-session מסתיים ואפליקציות מחזירות TRUE כדי לכבד את כוונת המשתמש (גם DefWindowProc כברירת מחדל מחזיר TRUE); על דחיית cleanup עד WM_ENDSESSION; על כך שהמערכת מציגה, אחרי 5 שניות, את ה-UI שמפרט אפליקציות שמונעות shutdown כדי שהמשתמש יוכל לכפות סיום; על משמעות סיביות ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL ב-lParam; על כך שאי אפשר להבחין בין shutdown ל-restart; ועל שמירת נתונים לעיתים קרובות כדי לצמצם את הכמות שנשמרת ביציאה. ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  2. Microsoft Learn, HandlerRoutine callback function. על events CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN שמתקבלים ב-handler שנרשם עם SetConsoleCtrlHandler; על כך ש-timeout ברירת המחדל של CTRL_CLOSE_EVENT הוא כ-5000 מילישניות ושל CTRL_SHUTDOWN_EVENT ל-service processes כ-20000 מילישניות; על כך ש-CTRL_LOGOFF/SHUTDOWN_EVENT מתקבלים בפועל רק ב-services כי אפליקציות אינטראקטיביות מסתיימות ב-logoff; ועל כך שה-handler רץ על thread נפרד. ↩ ↩2 ↩3 ↩4

  3. Microsoft Learn, Service Control Handler Function. על כך ש-services שמכריזים SERVICE_ACCEPT_PRESHUTDOWN מקבלים SERVICE_CONTROL_PRESHUTDOWN קודם, ואחריהם services של SERVICE_ACCEPT_SHUTDOWN מקבלים SERVICE_CONTROL_SHUTDOWN; על כך ש-grace period ברירת המחדל ב-shutdown הוא כ-20 שניות עם WaitToKillServiceTimeout כתקרה ב-restart של OS; על אי-הארכת הערך הזה; על כך ש-control handler חוזר תוך 30 שניות, מדווח STOP_PENDING עם wait hint, ומסר עבודה ארוכה ל-thread אחר; על סיום cleanup מהר ככל האפשר עם תפעול UPS בראש; ועל כך שה-SCM אינו מתחשב בתלויות ב-shutdown כברירת מחדל. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  4. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. על כך שה-runtime כבר לא מספק, מ-.NET 10, handlers ברירת מחדל ל-Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT (המקבילים ל-SIGTERM/SIGHUP ב-Unix); על כך שטיפול ברירת המחדל של ה-OS מסיים את האפליקציה מיד כך ש-AppDomain.ProcessExit ו-AssemblyLoadContext.Unloading כבר לא נורים; ועל כך שטיפול באותות שמתאים למודל האפליקציה נרשם בספריות ברמה גבוהה יותר או בקוד האפליקציה. ↩ ↩2 ↩3

  5. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. על כך ש-session של ה-kernel אינו נסגר אלא נכנס ל-hibernation תחת Fast Startup, עם מצב ה-kernel ו-device drivers שנשמר ל-hiberfil.sys; על כך ש-“Restart” תמיד מבצע boot מלא כי נדרש מצב Windows חדש לגמרי; על כך ש-Fast Startup דולק כברירת מחדל והשבתתו אינה מומלצת; ועל כך ש-Shutdown.exe כברירת מחדל עושה shutdown מלא עם אפשרות /hybrid להתנהגות היברידית. ↩ ↩2 ↩3 ↩4 ↩5

  6. Microsoft Learn, Shutdown Changes for Windows Vista. על כך שתשובות ל-WM_QUERYENDSESSION/WM_ENDSESSION ניתנות לדחייה ב-5 שניות כל אחת, ואחריהן המשתמש בוחר להמשיך או לבטל; על כך שאפליקציות console ואפליקציות בלי window גלוי אינן יכולות לבטל shutdown ומסתיימות אוטומטית אחרי 5 שניות בלי תשובה או בתשובת FALSE; על רישום סיבה עם ShutdownBlockReasonCreate כשצריך חסימה; ועל כך שאפליקציות אינן תלויות ביכולת לחסום shutdown. ↩ ↩2 ↩3

  7. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). על קריאה אליו בתחילת עבודה שאי אפשר להפריע לה כדי לרשום מחרוזת סיבה וקריאה ל-ShutdownBlockReasonDestroy בסיום; על כך שאפשר לקרוא לו רק מה-thread שיצר את ה-window; ועל השארת המחרוזת קצרה וברורה כי המשתמש קורא את הסיבה רק כמה שניות. ↩ ↩2 ↩3

  8. Microsoft Learn, SetConsoleCtrlHandler function. על כך ש-process שטען gdi32.dll או user32.dll מטופל כאפליקציית Windows ש-handlers של CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT שלה אינם נקראים; על ה-workaround של יצירת window מוסתר וטיפול ב-WM_QUERYENDSESSION/WM_ENDSESSION; ועל כך שפונקציות console עלולות לא לעבוד כרגיל בזמן טיפול באות. ↩

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). על כך שה-SCM ממתין אחרי notification של PRESHUTDOWN עד שה-service נעצר או עד timeout; על כך ש-timeout ברירת המחדל הוא 10 שניות מ-Windows 10 Creators Update (build 15063) ואילך ו-3 דקות לפני כן; על הגדרה עם ChangeServiceConfig2; ועל כך שעדכוני status ממשיכים במהלך SERVICE_STOP_PENDING. ↩ ↩2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). על רישום ל-restart בתרחישי crash, not-responding, update, ו-restart מחשב שנגרם מ-update; על ציון ארגומנטים של שורת פקודה ל-restart; על רישום לפני שבעיה מתרחשת, כשטיפול ב-WM_QUERYENDSESSION הוא ההזדמנות האחרונה בתרחיש update; על כך ש-processes שרצים פחות מ-60 שניות אינם מופעלים מחדש; על כך ש-restart אחרי crash או hang דורש הסכמת המשתמש; ועל כך שמעבר על restart של OS דורש shutdown עם EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS. ↩ ↩2

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). על כך ש-Windows Update שומר את ה-credentials של המשתמש האינטראקטיבי האחרון ומגדיר Autologon כשהוא מתחיל restart אוטומטי; על כך שהמשתמש נכנס אוטומטית אחרי ה-restart וה-session ננעל; על כך שה-credentials השמורים נמחקים אחרי sign-in מוצלח; ועל תצורה דרך Group Policy (DisableAutomaticRestartSignOn ואחרים). ↩ ↩2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). על כך ש-ReplaceFile משלב לפונקציה אחת את הצעדים המרובים שמתאימים לשמירה לקובץ חדש, שינוי שם זמני של המקור, שינוי שם הקובץ החדש ומחיקת המקור; על כך שהוא שומר את מאפייני הקובץ המקורי כמו זמן יצירה, DACL, הצפנה, דחיסה ו-named streams; ועל כך שה-backup, הקובץ שהוחלף וקובץ ההחלפה חייבים להיות על אותו volume. ↩ ↩2

  13. Microsoft Learn, File Caching. על כך שכתיבות הולכות ל-system cache כברירת מחדל ומוחלות לדיסק בכתיבה עצלה; על כך ש-FILE_FLAG_WRITE_THROUGH כותב נתונים לדיסק מיד; על FlushFileBuffers שעושה flush במפורש; ועל כך שמטא-נתונים של מערכת הקבצים תמיד ב-cache, כך שחיוב מטא-נתונים דורש flush או write-through. ↩ ↩2

  14. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. על כך שה-event הזה נמסר דרך WM_POWERBROADCAST במעבר בין סוללה לחשמל AC או כשהמטען הנותר יורד; ועל קריאה ל-GetSystemPowerStatus בקבלה כדי לבדוק ACLineStatus, BatteryFlag, BatteryLifePercent וחברים אחרים של SYSTEM_POWER_STATUS. ↩ ↩2

  15. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. על כך ש-restart רגיל רושם Event ID 1074 (איזה process יזם את ה-shutdown, בשם מי, ומאיזו סיבה); על כך ש-restart בלתי צפוי רושם Event ID 41 (Kernel-Power) ו-6008 (ה-shutdown הקודם היה בלתי צפוי); ועל שימוש ב-IDs האלה לבידוד סוג ה-restart. ↩ ↩2

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

בעיה ש-Shut down לא תיקן נעלמה אחרי Restart. למה?
ב-client OS מ-Windows 8 ואילך, כש-Fast Startup דולק (ברירת המחדל ברוב המחשבים שתומכים ב-hibernation), Shut down משתמש במנגנון שנקרא hybrid shutdown. המשתמש עושה sign-out, אבל מצב ה-kernel וה-drivers נשמר לקובץ hibernation וחוזר כמו שהוא ב-boot הבא. כלומר ליבת ה-OS לא עשתה reset. Restart, לעומת זאת, תמיד עושה boot מלא, ולכן תקלות ב-drivers וב-services מתאפסות. בנוהל isolation כותבים Restart במפורש, לא "לכבות ולהדליק מחדש". אם רוצים shutdown מלא משורת הפקודה, shutdown /s עושה זאת.
אפשר לעצור shutdown עד שהאפליקציה מסיימת לשמור?
אפשר לבקש המתנה זמנית, אבל אי אפשר לעצור אותו באמינות. אם רושמים מחרוזת סיבה עם ShutdownBlockReasonCreate רק בזמן שרצה פעולה שאי אפשר להפריע לה, הסיבה מופיעה במסך "האפליקציה הזו מונעת shutdown" והמשתמש יכול להחליט אם להמשיך או לבטל. המשתמש עדיין יכול לבחור המשך כפוי, ו-shutdown כפוי או restart בגלל update עלולים לא לחכות בכלל. הגישה הנכונה לכן אינה block, אלא autosave תכוף כך שפחות נתונים בסיכון, יחד עם cleanup שמתוכנן להסתיים תוך כמה שניות מה-exit notification.
עצירת Windows service שלי לוקחת הרבה זמן. אפשר להאריך את ה-grace period של ה-shutdown?
בתצורה כברירת מחדל שמקבלת SERVICE_CONTROL_SHUTDOWN, ה-grace period הוא בערך 20 שניות ותלוי בערך Registry WaitToKillServiceTimeout. שכתוב הערך הזה מצד האפליקציה כדי להאריך אותו אינו מומלץ. אם צריך grace period ארוך יותר, אפשר להכריז SERVICE_ACCEPT_PRESHUTDOWN ולקבל SERVICE_CONTROL_PRESHUTDOWN; מקבלים notification מוקדם מאחרים, ואת ה-timeout אפשר להגדיר עם ChangeServiceConfig2 (ברירת המחדל 10 שניות מ-Windows 10 Creators Update ואילך, ו-3 דקות לפני כן). PRESHUTDOWN, עם זאת, מעכב את כל ה-shutdown באותו מרווח, לכן מגבילים אותו למקרים שבאמת צריך, וביסוד מתכננים את עבודת ה-stop עצמה להסתיים תוך כמה שניות.
בטוח לעשות cleanup של shutdown ב-AppDomain.ProcessExit של .NET?
מומלץ לא להסתמך על זה. היסטורית ה-runtime רשם signal handlers כברירת מחדל, ו-ProcessExit נורה ב-CTRL_CLOSE_EVENT וב-CTRL_SHUTDOWN_EVENT, אבל מ-.NET 10 ה-runtime כבר לא מספק termination signal handlers כברירת מחדל, ו-ProcessExit כבר לא נורה במקרים האלה. מממשים cleanup בנתיב ה-notification שמתאים למודל האפליקציה: אפליקציות GUI משתמשות ב-FormClosing או SessionEnding (אלה notifications בשלב query, לכן מגבילים אותן לשמירות idempotent; cleanup שאפשר רק אחרי שה-exit מחויב שייך ל-hook של WM_ENDSESSION); Generic Host / Worker Service משתמשים ב-IHostApplicationLifetime וב-StopAsync; אפליקציות console משתמשות ב-SetConsoleCtrlHandler או PosixSignalRegistration.
איך מונעים השחתת קבצים מ-power loss פתאומי?
Power loss לא מביא שום notification, לכן האפשרות היחידה היא לכתוב באופן שלא נשבר מתי שלא תהיה החיתוך. הבסיס הוא לא לדרוס את הקובץ המקורי במקום: כותבים לגמרי לקובץ זמני על אותו volume, עושים flush, ומחליפים עם ReplaceFile (File.Replace ב-.NET). בפעולה רגילה זה משאיר אתכם מסוגלים לקרוא קובץ ישן שלם או קובץ חדש שלם, אבל ה-atomicity של ReplaceFile מעבר ל-power loss אינה מובטחת במפרט, לכן שומרים backup (הארגומנט השלישי) ומממשים recovery בזמן טעינה שמוודא את הקובץ הראשי ונופל ל-backup אם הוא שבור. גם, הצלחה מ-WriteFile לא אומרת שהנתונים הגיעו לדיסק, לכן ב-checkpoints חשובים מאשרים את הכתיבה עם FlushFileBuffers או FILE_FLAG_WRITE_THROUGH. ב-device PC התצורה הסטנדרטית היא לשלב את זה עם UPS, לזהות מעבר לסוללה, ולהוביל ל-shutdown בטוח.

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג