Shutdown ב-Windows מנקודת המבט של האפליקציה — לשרוד exit notifications, restart ו-power loss נכון
· עודכן בתאריך: · Go Komura · 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.
flowchart TB
accTitle: תכנון שמירה משותף ל-exit notifications ול-power loss
accDescr: שמירה שגרתית משאירה את ההפרש שלא נשמר קטן; עם notification בא cleanup קצר, ובלי אחד הנתונים השמורים מאומתים ומשוחזרים לפני שהעבודה ממשיכה
daily["שמירה בכל אבן דרך"] --> event{"יש notification ביציאה?"}
event -->|"כן"| close["שומרים רק את ההפרש שנשאר ויוצאים"]
event -->|"לא"| lost["אין cleanup אפשרי במקום"]
close --> boot["אימות ו-recovery ב-startup הבא"]
lost --> boot
boot --> resume["המשך העבודה"]
איור 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 לכל אחד.
flowchart TB
accTitle: חושבים על sign-out ועל יציאת מערכת בנפרד
accDescr: Sign-out מסיים את אפליקציות המשתמש בזמן ש-services ממשיכים לרוץ; shutdown או restart של המערכת גם עוצרים services; power loss אינו מודיע לאף אחד
signout["Sign-out"] --> user["אפליקציות המשתמש יוצאות"]
signout -.-> alive["Services ממשיכים לרוץ"]
system["Shutdown או restart"] --> user
system --> svc["גם services נעצרים"]
power["Power loss"] --> none["אין 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
flowchart TB
accTitle: Fast Startup מול boot מלא
accDescr: Shutdown עם Fast Startup דולק שומר ומשחזר את מצב ה-kernel וה-drivers, בעוד shutdown מלא או restart מאתחלים הכול ב-boot מלא
s["Shutdown"] --> q{"להשתמש ב-Fast Startup?"}
q -->|"כן"| save["Hibernate ל-kernel ול-drivers"]
save --> restore["שחזור המצב ב-startup הבא"]
q -->|"לא"| full["Shutdown מלא"]
full --> boot["אתחול ב-boot מלא בפעם הבאה"]
r["Restart"] --> boot
איור 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
flowchart TB
accTitle: ה-query של ה-GUI ותוצאת היציאה
accDescr: גם אחרי החזרת TRUE ל-WM_QUERYENDSESSION אפליקציה אחרת יכולה לבטל את היציאה, כך ש-cleanup אחרי commit רץ רק כש-wParam של WM_ENDSESSION הוא TRUE
query["WM_QUERYENDSESSION"] --> reply["מחזירים TRUE מיד, כעיקרון"]
reply --> result["WM_ENDSESSION"]
result --> yes{"האם wParam הוא TRUE?"}
yes -->|"כן"| cleanup["Cleanup אחרי commit"]
yes -->|"לא"| running["היציאה בוטלה; ממשיכים לרוץ"]
איור 4: אל תבלבלו בין התשובה שמתירה את היציאה לבין ה-notification שהיציאה מחויבת.
יש מקרים שבהם אפשר להחזיר FALSE כדי לסרב ליציאה, אבל הכלל הוא לכבד את כוונת המשתמש לצאת. אפליקציה שמסרבת מוצגת כאפליקציה שמונעת shutdown. לאפליקציות console ולאפליקציות בלי window גלוי יש גם אילוצים: בתצורה רגילה, אפליקציה שלא מגיבה תוך 5 שניות יכולה להסתיים אוטומטית. מתייחסים לחסימה כטיפול החריג של סעיף 4 ואל תשתמשו בה לשמירה רגילה.6
3.2. כ-5 שניות אינן ערובה שאפשר לסיים לשמור
אם דוחים את התשובה בכ-5 שניות בשלב WM_QUERYENDSESSION או בשלב WM_ENDSESSION, המערכת מציגה את המסך שמפרט את האפליקציות שמונעות shutdown, והמשתמש יכול לבחור לכפות את ה-shutdown. אחרי סיום כפוי אין הזדמנות לסיים את שאר השמירה.6
הטיפול הוא לשמור באופן שגרתי ולצמצם את ההפרש ביציאה. מחנים מצב עבודה שלא נשמר במיקום זמני ומשחזרים אותו ב-startup הבא. אל תתכננו את האפליקציה להציג דיאלוג אישור בזמן shutdown ולהמתין. מתייחסים לאישור יציאה רגיל ולבקשת יציאה מה-OS כשני דברים נפרדים.1
flowchart TB
accTitle: משאירים את העבודה ביציאה קטנה
accDescr: תכנון ששומר באופן שגרתי משאיר הפרש קטן ביציאה, בעוד תכנון שצובר בזיכרון עד היציאה אינו נכנס ל-grace period הקצר ומסתכן באובדן דרך סיום כפוי
good["שמירה שגרתית"] --> small["ההפרש שנשאר ביציאה קטן"]
small --> fast["יציאה עם cleanup קצר"]
bad["צבירה בזיכרון עד היציאה"] --> large["שמירת הכול ביציאה"]
large --> risk["מעט מדי 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.
flowchart TB
accTitle: חלוקת טיפול היציאה ב-WinForms וב-WPF
accDescr: FormClosing ו-SessionEnding שומרים snapshot שבטוח גם אם היציאה מבוטלת, ו-hook על WM_ENDSESSION עם TRUE מבצע את ה-cleanup שאי אפשר לבטל
notify["שלב query"] --> forms["FormClosing"]
notify --> wpf["SessionEnding"]
forms --> snapshot["שמירת snapshot idempotent"]
wpf --> snapshot
final["WM_ENDSESSION עם TRUE"] --> hook["קבלה ב-WndProc או ב-hook"]
hook --> cleanup["עבודה אחרי 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. כשהעבודה מסתיימת, מנקים גם את הסיבה וגם את דגל ההגנה.
flowchart TB
accTitle: חלוקת תפקידים בחסימת יציאה זמנית
accDescr: רק בזמן שעבודה שאי אפשר להפריע לה רצה, משלבים רישום סיבה עם סירוב ל-query ומנקים את שניהם בסיום; shutdown כפוי בידי המשתמש עדיין אי אפשר למנוע
start["עבודה שאי אפשר להפריע לה מתחילה"] --> reason["רושמים את הסיבה ומגדירים את דגל ההגנה"]
reason --> work["עיבוד על worker"]
work --> done["סיום; מנקים את הסיבה ואת הדגל"]
work -.-> request["Query יציאה מגיע בינתיים"]
request --> refuse["מחזירים FALSE ומציגים את הסיבה"]
refuse --> choice{"החלטת המשתמש"}
choice -->|"ביטול"| keep["ממשיכים לרוץ"]
choice -->|"כפייה"| terminate["אפשר לסיים"]
איור 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
flowchart TB
accTitle: תנאים לקבלת exit notifications של console
accDescr: Console אינטראקטיבי יכול לקבל את ה-notification של הסגירה אבל אינו יכול לצפות ל-notifications של logoff או shutdown, ואפילו service צריך נתיב notification אחר ברגע שהוא טוען את DLLs של GUI
console["Process של console"] --> close["סגירה הולכת ל-control handler"]
console --> q{"ממתינים ל-LOGOFF או ל-SHUTDOWN?"}
q -->|"Session אינטראקטיבי"| no["אי אפשר להסתמך על ה-notification הזה"]
q -->|"Service"| dll{"נטענו DLLs של GUI?"}
dll -->|"לא"| signal["מטפלים ב-control signal הרלוונטי"]
dll -->|"כן"| window["מקבלים דרך 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.
flowchart TB
accTitle: הסרת התלות ב-handler ברירת המחדל לסיום של .NET
accDescr: ה-runtime הישן חיבר את האותות הרלוונטיים ל-ProcessExit, אבל מ-.NET 10 אין handler ברירת מחדל, לכן משתמשים בנתיב ה-notification של מודל האפליקציה
signal["אות CLOSE או SHUTDOWN"] --> old["טיפול ברירת המחדל של runtime ישן"]
old --> event["מעלה ProcessExit ודומים"]
signal --> modern["אין טיפול ברירת מחדל מ-.NET 10 ואילך"]
modern --> own{"האם האפליקציה מטפלת בזה?"}
own -->|"לא"| os["מסתיים בטיפול ברירת המחדל של ה-OS"]
own -->|"כן"| handle["Cleanup שמתאים למודל"]
איור 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 קטנה.
flowchart TB
accTitle: עיבוד ה-stop של ה-Host ו-grace period היציאה החיצוני
accDescr: ה-stop של Generic Host מרוכז ב-StopAsync, אבל הגדרת ה-timeout של HostOptions אינה מאריכה את grace period היציאה של ה-OS או ה-SCM, לכן בודקים את שניהם
outer["בקשת יציאה מה-OS או מה-SCM"] --> host["נתיב ה-stop של ה-Host"]
host --> stop["Cleanup ב-StopAsync"]
stop --> inner["מגדירים את זמן ה-stop בצד ה-Host"]
outer -.-> limit["ה-grace period החיצוני קיים בנפרד"]
inner --> check["מודדים אם זה מסתיים מהר"]
limit --> check
איור 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 שהוגדר |
flowchart TB
accTitle: שלבי exit notification ל-services
accDescr: ה-SCM מודיע קודם ל-services שמקבלים PRESHUTDOWN, ממתין שהם ייעצרו או שיפוג הזמן, ואז ממשיך ל-notification הרגיל של SHUTDOWN
start["Shutdown המערכת מתחיל"] --> pre["מודיעים ל-services שמקבלים PRESHUTDOWN"]
pre --> wait["ממתינים ל-stop או למועד שהוגדר"]
wait --> shut["מודיעים ל-services שמקבלים SHUTDOWN"]
shut --> proceed["אחרי ה-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
flowchart TB
accTitle: חלוקת עבודה בין control handler של ה-service ל-worker
accDescr: ה-control handler מדווח stop pending, מסמן ל-worker, וחוזר מיד; ה-worker עושה את ה-cleanup ואת דיווח ההתקדמות ולבסוף מדווח stopped
handler["Control handler"] --> pending["דיווח STOP_PENDING"]
pending --> signal["סימון ל-worker לעצור"]
signal --> back["ה-handler חוזר מיד"]
signal --> worker["ה-worker מנקה"]
worker --> report["דיווח התקדמות אם זה לוקח זמן"]
report --> stopped["דיווח 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 יכול להיחתך.
flowchart TB
accTitle: שמירה ודיווח התקדמות ב-recovery callback
accDescr: ה-recovery callback ש-WER מפעיל ממשיך לקרוא ל-ApplicationRecoveryInProgress בתוך מרווח ה-ping בזמן שמירה, ומודיע ApplicationRecoveryFinished בסיום
reg["רישום מראש של recovery callback"] --> crash["Crash; WER מפעיל אותו"]
crash --> save["שמירת הנתונים שעובדים עליהם"]
save --> progress["דיווח התקדמות בתוך מרווח ה-ping"]
progress --> completed{"השמירה הסתיימה?"}
completed -->|"עדיין לא"| save
completed -->|"סיום"| done["הודעה ש-recovery הסתיים"]
progress -.-> timeout["אפשר לחתוך אם הדיווחים נעצרים"]
איור 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
flowchart TB
accTitle: רישום restart, sign-in ושחזור נתונים
accDescr: מול רישום restart מראש, crash דורש הסכמת המשתמש, ואפליקציות משתמש אחרי restart של OS צריכות שחזור session דרך ARSO או דומה ושחזור המצב השמור
reg["רישום ל-restart לפני שבעיה מתרחשת"] --> crash["Crash או not responding"]
crash --> consent["קבלת הסכמת המשתמש"]
consent --> app["Restart האפליקציה"]
reg --> update["Restart של OS עם הדגלים הנדרשים"]
update --> session["שחזור ה-session דרך ARSO או דומה"]
session --> app
app --> data["קריאת נקודת השחזור השמורה"]
session -.-> policy["בדיקת המדיניות ותנאי ה-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
flowchart TB
accTitle: שמירת קובץ ו-recovery ב-startup הבא
accDescr: כותבים לקובץ זמני על אותו volume, עושים flush, מחליפים תוך שמירת backup, ואז מאמתים את הקובץ הראשי ב-startup ונופלים ל-backup אם צריך
tmp["קובץ זמני על אותו volume"] --> write["כתיבה עד הסוף ו-flush"]
write --> replace["החלפה; התוכן הישן הולך ל-.bak"]
replace -.-> boot["Startup הבא"]
boot --> valid{"האם הקובץ הראשי שלם?"}
valid -->|"כן"| main["קוראים את הקובץ הראשי"]
valid -->|"לא"| backup["נופלים ל-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
flowchart TB
accTitle: הגבול בין הצלחת כתיבה להתמדה
accDescr: כתיבה רגילה מגיעה לאחסון מ-cache של ה-OS בהשהיה; flush או write-through דוחפים אותה לחיוב, אבל אילוצי ה-cache בצד המכשיר נשארים
write["WriteFile מצליח"] --> cache["יכול להיות ב-cache של ה-OS"]
cache --> delayed["כתיבה עצלה"]
cache --> flush["Flush ב-checkpoint"]
delayed --> device["מוחל על האחסון"]
flush --> device
device -.-> limit["מגבלות ה-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
flowchart TB
accTitle: מזיהוי UPS עד השלמת shutdown
accDescr: מזהים את מעבר ה-UPS לסוללה דרך נתיב ה-notification המתאים, בודקים את מצב החשמל, ממשיכים לשמירה ול-shutdown, ומתכננים את כל הרצף להיכנס לזמן ריצת הסוללה
outage["הפסקה; ה-UPS עובר לסוללה"] --> notice["Power notification בנתיב המתאים"]
notice --> check["בדיקת מצב החשמל והמטען הנותר"]
check --> save["השהיית המדידה ושמירה"]
save --> req["בקשת shutdown מה-OS"]
req --> done["השלמת ה-cleanup ויציאת ה-OS"]
done -.-> time["להכניס את כל הרצף לזמן הריצה"]
איור 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
flowchart TB
accTitle: הרחבת אימות shutdown בשלבים
accDescr: מנסים את notifications של ה-GUI וכל פעולת יציאה בסביבת הבדיקה, מאשרים recovery אחרי עצירה פתאומית ב-VM, ואז מאמתים עמידות ל-power loss כולל אחסון על חומרה שקולה לפרודקשן
prep["הכנת מחשב הבדיקה והנתונים"] --> notify["ניסיון נתיבי notification ופעולות יציאה"]
notify --> time["מדידת זמן ה-cleanup"]
time --> vm["אישור recovery אחרי עצירת VM פתאומית"]
vm --> real["אימות על חומרה אמיתית כולל אחסון"]
real --> check["אישור הנתונים וה-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
flowchart TB
accTitle: בחירת מה לחקור מ-event log של shutdown
accDescr: מאשרים את ה-process היוזם ואת הסיבה מ-1074, ומתייחסים ל-41 ול-6008 כרמזים לסיום בלתי צפוי להצליב עם מידע מסביב כמו bug check code ו-dumps
log["System event log"] --> normal["1074: process יוזם וסיבה"]
normal --> cleanup["חקירת טיפול ה-notification ונתיב השמירה"]
log --> unexpected["41 ו-6008: סיום בלתי צפוי"]
unexpected --> evidence["בדיקת BugcheckCode ו-dumps"]
evidence --> classify["בידוד 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
flowchart TB
accTitle: בדיקה סופית מטיפול יציאה עד recovery
accDescr: משאירים מצב שמור במהלך עיבוד רגיל, עושים cleanup קטן ב-exit notification, ומאמתים את הנתונים שנשארים גם בלי notification כדי לשחזר ב-startup הבא
daily["משאירים מצב שמור באופן שגרתי"] --> endq{"יש exit notification?"}
endq -->|"כן"| short["יוצאים עם cleanup קצר"]
endq -->|"לא"| prior["המצב השמור האחרון הוא כל מה שיש"]
short --> nextboot["אימות ו-recovery ב-startup הבא"]
prior --> nextboot
nextboot --> restart["חוזרים לעבודה בתנאים הנדרשים"]
איור 20: אל תפרידו את מימוש ה-exit event משמירה שגרתית ו-recovery ב-startup הבא.
לבסוף, מנסים את נתיבי ה-notification ואת הזמן הנדרש במחשב בדיקה, ומאשרים גם את ה-recovery אחרי עצירה פתאומית. בחקירה בדיעבד, משתמשים ב-1074 / 41 / 6008 כרמזים לבידוד יציאה מתוכננת מסיום בלתי צפוי.
בפעם הבאה שמוסיפים תכונה, שואלים “אם exit notification מגיע במהלך העיבוד הזה, או ששולפים את התקע, מה יישאר ב-startup הבא?” הכללת התשובה הזו בתכנון היא מה שמגן עליכם מלגלות השחתת נתונים רק בבוקר שלמחרת.
מאמרים קשורים
- איך מחליפים EXE או DLL שבשימוש — Restart Manager ובעיית “הקובץ בשימוש” בעדכונים אוטומטיים
- איך בונים ומפעילים Windows service — מבחירה בין Task Scheduler ל-service עד הפיכת BackgroundService ל-service
- Sleep, hibernation, Modern Standby ואפליקציות long-running — מניעת “זה נעצר באמצע הלילה” בתכנון
- מעמקי ה-I/O ב-Windows (פרק 4) — Cache Manager: מתי WriteFile באמת מגיע לדיסק
- איך משאירים לוגים ו-crash dump כשאפליקציית Windows קורסת
- רשימת בדיקה לניהול בטוח של child processes באפליקציית Windows
תחומי ייעוץ קשורים
KomuraSoft LLC מטפלת בתכנון ובמימוש של אמצעי נגד shutdown ו-power loss ל-device PCs ולאפליקציות long-running, בחקירת שורש של השחתת נתונים וכישלונות “זה היה למטה בבוקר” שמתחילים מ-restart של Windows Update או מ-sign-out, ובסקירות תכנון של עיבוד stop של Windows service ו-recovery אוטומטי. אפשר להתחיל משלב של “משהו נשבר בכל פעם שעושים shutdown, אבל אני לא יודע מאיפה להתחיל”.
קישורים
-
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
-
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
-
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
-
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
-
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
-
Microsoft Learn, Shutdown Changes for Windows Vista. על כך שתשובות ל-WM_QUERYENDSESSION/WM_ENDSESSION ניתנות לדחייה ב-5 שניות כל אחת, ואחריהן המשתמש בוחר להמשיך או לבטל; על כך שאפליקציות console ואפליקציות בלי window גלוי אינן יכולות לבטל shutdown ומסתיימות אוטומטית אחרי 5 שניות בלי תשובה או בתשובת FALSE; על רישום סיבה עם ShutdownBlockReasonCreate כשצריך חסימה; ועל כך שאפליקציות אינן תלויות ביכולת לחסום shutdown. ↩ ↩2 ↩3
-
Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). על קריאה אליו בתחילת עבודה שאי אפשר להפריע לה כדי לרשום מחרוזת סיבה וקריאה ל-ShutdownBlockReasonDestroy בסיום; על כך שאפשר לקרוא לו רק מה-thread שיצר את ה-window; ועל השארת המחרוזת קצרה וברורה כי המשתמש קורא את הסיבה רק כמה שניות. ↩ ↩2 ↩3
-
Microsoft Learn, SetConsoleCtrlHandler function. על כך ש-process שטען gdi32.dll או user32.dll מטופל כאפליקציית Windows ש-handlers של CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT שלה אינם נקראים; על ה-workaround של יצירת window מוסתר וטיפול ב-WM_QUERYENDSESSION/WM_ENDSESSION; ועל כך שפונקציות console עלולות לא לעבוד כרגיל בזמן טיפול באות. ↩
-
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
-
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
-
Microsoft Learn, Winlogon automatic restart sign-on (ARSO). על כך ש-Windows Update שומר את ה-credentials של המשתמש האינטראקטיבי האחרון ומגדיר Autologon כשהוא מתחיל restart אוטומטי; על כך שהמשתמש נכנס אוטומטית אחרי ה-restart וה-session ננעל; על כך שה-credentials השמורים נמחקים אחרי sign-in מוצלח; ועל תצורה דרך Group Policy (DisableAutomaticRestartSignOn ואחרים). ↩ ↩2
-
Microsoft Learn, ReplaceFileW function (winbase.h). על כך ש-ReplaceFile משלב לפונקציה אחת את הצעדים המרובים שמתאימים לשמירה לקובץ חדש, שינוי שם זמני של המקור, שינוי שם הקובץ החדש ומחיקת המקור; על כך שהוא שומר את מאפייני הקובץ המקורי כמו זמן יצירה, DACL, הצפנה, דחיסה ו-named streams; ועל כך שה-backup, הקובץ שהוחלף וקובץ ההחלפה חייבים להיות על אותו volume. ↩ ↩2
-
Microsoft Learn, File Caching. על כך שכתיבות הולכות ל-system cache כברירת מחדל ומוחלות לדיסק בכתיבה עצלה; על כך ש-FILE_FLAG_WRITE_THROUGH כותב נתונים לדיסק מיד; על FlushFileBuffers שעושה flush במפורש; ועל כך שמטא-נתונים של מערכת הקבצים תמיד ב-cache, כך שחיוב מטא-נתונים דורש flush או write-through. ↩ ↩2
-
Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. על כך שה-event הזה נמסר דרך WM_POWERBROADCAST במעבר בין סוללה לחשמל AC או כשהמטען הנותר יורד; ועל קריאה ל-GetSystemPowerStatus בקבלה כדי לבדוק ACLineStatus, BatteryFlag, BatteryLifePercent וחברים אחרים של SYSTEM_POWER_STATUS. ↩ ↩2
-
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
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
הרשת עובדת אבל Windows מציג "אין אינטרנט" — לבודד NCSI, DNS, Proxy ו-VPN ב-Windows
למה Windows מציג "אין אינטרנט" בזמן שהרשת עובדת — נקודת המוצא היא קביעת הקישוריות של NCSI. כאן מפרידים בין DNS, proxy, VPN ו-captive port...
מה Fast Startup באמת עושה — למה "Shut down" ב-Windows אינו אותו דבר כמו Restart
Shutdown ב-Windows הוא hybrid shutdown כברירת מחדל, ושומר את ה-kernel ואת ה-drivers ל-hiberfil.sys. למה רק restart מאפס אותם, ומתי לכבות ...
Time Travel Debugging — להקליט ולהריץ אחורה באגים שלא משתחררים באפליקציות ארוכות-ריצה
באג פעם בחודש משאיר ב-crash dump רק את התוצאה. מקליטים ומריצים אחורה את הביצוע עם WinDbg Time Travel Debugging (TTD): TTD.exe, ring buffe...
למה arguments נשברים — כללי command-line arguments ב-Windows
Windows מעביר ל-CreateProcess מחרוזת אחת שהמקבל מפצל. מכסה את כללי CommandLineToArgvW, CRT ו-.NET, ArgumentList, ובניה ב-C++.
סיום התמיכה ב-printer drivers של Windows — איך אפליקציות עסקיות מתכוננות להדפסת דוחות ותוויות
Microsoft מפסיקה בהדרגה printer drivers מסוג v3/v4. מה Windows protected print mode מסיר, ואיך עושים inventory ומכינים הדפסת דוחות ותוויו...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
יישומים עסקיים, חיבור התקנים וכלי תקשורת, מהגדרת הדרישות ועד הפיתוח.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- בעיה ש-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 בטוח.