תכנון שמירת יומנים ו-dump בקריסת יישום Windows
· עודכן בתאריך: · Go Komura · פיתוח Windows, טיפול בחריגות, יומנים, WER, dump של קריסה, חקירת תקלות
בחקירת תקלות ביישומי Windows, הכי קשה זה המצב שבו רק יודעים שהיישום קרס, אבל אין שום עדות למה.
הבעיה הזו מחמירה במיוחד בפרויקטים כמו:
- קורס רק בסביבת הלקוח
- קורס רק אחרי הפעלה ארוכת טווח
- שיעור שחזור נמוך ב-WPF/WinForms/שירות Windows/יישום שרץ ברקע
- מעורבים COM, P/Invoke, DLL נייטיבי, SDK של ספק
- יש “רק את הודעת החריגה” אבל אין הקשר לפני כן
עם זאת, בכנות מראש: אי אפשר להבטיח “בוודאות” שמירת יומן רק על ידי התהליך שקורס. כשלוקחים בחשבון קריסת stack, השחתת זיכרון, fast fail, סיום כפוי וניתוק חשמל, היומן האחרון בתוך התהליך (in-process) הוא במהותו best effort.
flowchart TB
accTitle: היומן האחרון בתוך התהליך הוא best effort
accDescr: תרשים המראה שכשלוקחים בחשבון קריסת stack, השחתת זיכרון, fast fail, סיום כפוי וניתוק חשמל, לא ניתן להבטיח בוודאות שמירת יומן רק על ידי התהליך הקורס, והיומן האחרון בתוך התהליך הוא במהותו best effort.
a1["קריסת stack · השחתת זיכרון"] --> a4["היומן האחרון בתהליך"]
a2["fast fail · סיום כפוי"] --> a4
a3["ניתוק חשמל"] --> a4
a4 --> a5["במהותו best effort"]
a5 -.-> a6["אי אפשר להבטיח 'בוודאות'"]
איור 1: התהליך הקורס בלבד לא יכול להבטיח “בוודאות” את שמירת היומן האחרון.
בעבודה בפועל, המטרה היא תצורה שלא מסתמכת רק על התהליך הקורס. כלומר, חושבים בשלוש שכבות:
- יומן כרונולוגי בזמן רגיל
- סמן הקריסה הסופי ברגע הנפילה
- ראיית קריסה שנשמרת בצד מערכת ההפעלה או בתהליך נפרד
במאמר הזה, על בסיס הנחת יסוד של יישומי שולחן עבודה ל-Windows, יישומים שרצים ברקע, שירותי Windows וכלי חיבור לציוד, נסדר שיטות עבודה מומלצות לשמירה על יכולת החקירה גם כשהיישום קורס מחריגה שמקורה בבאג.
1. המסקנה קודם
קודם רק המסקנות, בשורה.
- הכי חשוב: לא להמר על “היומן האחרון” רק ב-handler אחד בתוך התהליך.
- הכי בטוח בעבודה בפועל: שילוב יומן רגיל + סמן קריסה סופי + WER LocalDumps.
- בהפעלה ארוכת טווח, חיבור לציוד, plugins, או ערבוב SDK נייטיבי, הוספת תהליך ניטור (watchdog/launcher/service) מחזקת מאוד.
- ב-handler של קריסה, לא לעשות עיבוד כבד הוא כלל ברזל. מוציאים דחיסה, שליחת HTTP, פתרון DI, דו-שיח UI ובניית JSON מורכב.
- בקריסה, שומרים רק בקצרה מקומית, ודחיסה/העלאה/התראה עוברים להפעלה הבאה או לתהליך נפרד.
- שימוש ב-
ThreadExceptionשל WinForms אוDispatcherUnhandledExceptionשל WPF כדי להאריך חיים למראית עין מסוכן מול חריגה שמקורה בבאג. - גם ב-.NET וגם ב-native, בטוח יותר לבסס חריגה שחשודה במצב שבור על “רישום וסיום” ולא “התאוששות”.
- אם לוקחים dump, שמירת PDB והבינארי המופץ חייבת להיעשות באותו זמן, אחרת אי אפשר לקרוא בדיעבד.
בקיצור, שיטת העבודה המומלצת היא “לא לנסות לעשות הכול ברגע הקריסה. לחלק תפקידים לפני הקריסה, ברגע הקריסה ואחריה”.
flowchart TB
accTitle: חלוקת תפקידים לפני, ברגע ואחרי הקריסה
accDescr: תרשים המראה ששיטת העבודה המומלצת היא לא לעשות הכול ברגע הקריסה, אלא לחלק תפקידים - לפני הקריסה יומן רגיל, ברגע הקריסה כתיבה מקומית קצרה, ואחרי הקריסה דחיסה והעלאה והתראה.
b1["לפני: שומרים יומן רגיל"] --> b2["ברגע הקריסה: כתיבה מקומית קצרה בלבד"]
b2 --> b3["אחרי: דחיסה · העלאה · התראה"]
b3 -.-> b4["בהפעלה הבאה או בתהליך נפרד"]
איור 2: לא הכול ברגע הקריסה - מחלקים תפקידים בין לפני, ברגע, ואחרי.
1.1 מונחים שהמאמר משתמש בהם
המונחים שישמשו בהמשך ללא הסבר נוסף, מרוכזים כאן מראש.
| מונח | פירוש/הגייה | משמעות |
|---|---|---|
| WER | Windows Error Reporting, דיווח שגיאות של Windows | מנגנון Windows שתופס ורושם סיום חריג של אפליקציה בצד מערכת ההפעלה. ההגדרה ששומרת dump מקומי נקראת LocalDumps |
| dump / minidump | crash dump | קובץ ששומר את תוכן הזיכרון של התהליך ברגע הקריסה. אפשר לראות בדיעבד threads, stack ומודולים |
| PDB | Program Database | קובץ סמלים שנוצר בזמן ה-build. בלעדיו, גם כשפותחים dump לא מתקבלים שמות פונקציות או מספרי שורות |
| in-process | בתוך התהליך | עיבוד בתוך התהליך הקורס עצמו. ההפך: תהליך נפרד |
| best effort | מאמץ מיטבי | תכונה של “נשמר אם מסתדר, אין הבטחה”. כך הוא היומן ה-in-process ברגע הקריסה |
fast fail / __fastfail |
סיום כשל מהיר | מנגנון שכשמחליטים שהמצב שבור, מסיים מיד במינימום פעולות בלי ניקיון. ב-native זה __fastfail, ב-.NET המקביל הוא Environment.FailFast |
| watchdog | תהליך ניטור | תהליך נפרד שמשגיח מבחוץ על הפעלה, סיום וחיוּת התהליך הראשי. אפשר לבנות גם כ-launcher או כשירות אב |
| heartbeat | איתות חיים | איתות שמודיע ל-watchdog בקביעות “עדיין פועל” |
| נתיב UNC | Universal Naming Convention | נתיב שיתוף רשת בפורמט \\server\share\.... שימוש בו בזמן קריסה גורם להמתנה עקב ניתוק רגעי או אישורי גישה |
| ACL | Access Control List, רשימת בקרת גישה | ההגדרה מי רשאי לקרוא ולכתוב לתיקייה. גורם קבוע ל”ירי בלק” של dump ויומן |
| SEH | Structured Exception Handling, טיפול חריגות מובנה | מנגנון החריגות הנייטיבי של Windows. SetUnhandledExceptionFilter שייך לכאן |
| CRT | C Runtime | מימוש הספרייה הסטנדרטית של C/C++. יש לו מסלולי סיום עצמאיים, בנפרד מ-SEH |
| session | session ID | ערך לזיהוי “על איזה מופע הפעלה מדובר”. המפתח להצלבת יומן, dump ורישומי watchdog |
מפת הידע של המאמר
כדי שיישום Windows יאפשר לאתר את הסיבה גם כשהוא קורס מחריגה שמקורה בבאג, יעיל תכנון שלא מסתמך רק על היומן של התהליך הקורס עצמו, אלא מחלק את הראיות לשלוש שכבות - יומן כרונולוגי רגיל, סמן קריסה סופי ברגע הנפילה, ו-dump באמצעות WER LocalDumps. שימוש ב-AppDomain.UnhandledException, וב-ThreadException של WinForms או DispatcherUnhandledException של WPF כדי להאריך חיים למראית עין מסוכן; בטוח יותר להשתמש בהם כשער לרישום ואז לסיים באמצעות API של סיום מיידי כמו Environment.FailFast - אך קריאה ל-FailFast בתוך UnhandledException מחליפה את סיבת ה-dump. ב-C++ נייטיבי צריך לקלוט לא רק SEH אלא גם את מסלולי הסיום של ה-CRT, וכאשר מדובר בהפעלה 24 שעות או בבקרת ציוד, הוספת תהליך ניטור מאפשרת לזהות מבחוץ גם exit code וגם מספר הפעלות מחדש.
flowchart LR
accTitle: מפת הידע של תכנון היומן וה-dump בקריסת יישום Windows
accDescr: תרשים המראה שתכנון שלא מסתמך על ראיה רק בתוך התהליך הקורס מתבסס על חלוקת תפקידים בין שלוש שכבות - יומן רגיל, סמן קריסה סופי, ו-WER LocalDumps - על שילוב handler-ים שונים כמו AppDomain.UnhandledException ו-FailFast, ועל זיהוי חיצוני על ידי תהליך ניטור.
crash_time_logging_design["תכנון היומנים והראיות בעת קריסה"]
wer_localdumps["WER LocalDumps"]
application_log["יומן רגיל (יומן כרונולוגי)"]
fatal_crash_marker["סמן הקריסה הסופי"]
seh["SEH (טיפול חריגות מובנה)"]
crt_termination_handler["מסלולי הסיום של ה-CRT ושל זמן הריצה של C++"]
watchdog_process["תהליך ניטור (watchdog)"]
high_reliability_operation_requirement["דרישות מחמירות כמו הפעלה 24/7 ובקרת ציוד"]
post_restart_processing["עיבוד המשך אחרי ההפעלה הבאה"]
session_id_correlation["הצלבת ראיות לפי session ID"]
dotnet_unhandledexception["AppDomain.UnhandledException"]
winforms_threadexception["Application.ThreadException(WinForms)"]
unexpected_exception_continuation["המשך העיבוד אחרי חריגה בלתי צפויה שמקורה בבאג"]
wpf_dispatcherunhandledexception["Application.DispatcherUnhandledException(WPF)"]
unobserved_task_exception["TaskScheduler.UnobservedTaskException"]
environment_failfast["Environment.FailFast"]
windows_application_event_log["יומן אירועי היישומים של Windows"]
dump_folder_acl["ACL של תיקיית שמירת ה-dump"]
crash_dump["crash dump (תמונת קריסה)"]
pdb["PDB (מסד נתוני התוכנית)"]
windbg["WinDbg"]
minidumpwritedump["MiniDumpWriteDump"]
wer_file_registration["רישום קובץ ליומן באמצעות WerRegisterFile"]
crash_time_logging_design -->|"מחייב"| application_log
crash_time_logging_design -->|"מחייב"| fatal_crash_marker
crash_time_logging_design -->|"מחייב"| wer_localdumps
crash_time_logging_design -.->|"מחייב"| seh
crash_time_logging_design -.->|"מחייב"| crt_termination_handler
watchdog_process -->|"מענה מומלץ ל"| high_reliability_operation_requirement
fatal_crash_marker -->|"צריך לקדום ל"| post_restart_processing
fatal_crash_marker -.->|"מחייב"| session_id_correlation
fatal_crash_marker -->|"מענה מומלץ ל"| dotnet_unhandledexception
winforms_threadexception -->|"שימוש לא מומלץ ל"| unexpected_exception_continuation
wpf_dispatcherunhandledexception -->|"שימוש לא מומלץ ל"| unexpected_exception_continuation
unobserved_task_exception -->|"שימוש לא מומלץ ל"| fatal_crash_marker
environment_failfast -->|"משתמש ב"| windows_application_event_log
fatal_crash_marker -->|"מענה מומלץ ל"| seh
fatal_crash_marker -->|"מענה מומלץ ל"| crt_termination_handler
wer_localdumps -.->|"מחייב"| dump_folder_acl
crash_dump -.->|"מחייב"| pdb
crash_dump -->|"נבדק באמצעות"| windbg
minidumpwritedump -->|"מממש את"| crash_dump
watchdog_process -.->|"משתמש ב"| minidumpwritedump
wer_file_registration -->|"משתמש ב"| application_log
wer_localdumps -->|"צריך לקדום ל"| wer_file_registration
wer_localdumps -->|"מממש את"| crash_dump
fatal_crash_marker -->|"מענה מומלץ ל"| winforms_threadexception
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 24, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
2. למה אי אפשר להבטיח “וודאות” רק בתוך התהליך
אם זה נשאר מעורפל, התכנון מתנדנד.
2.1 לפעמים ההקשר של ה-thread שקרס עצמו שבור
ה-hook של חריגה בלתי מטופלת או exception filter ברמה העליונה עלולים לפעול בתוך ההקשר של ה-thread השבור. בשלב הזה, זה קורה בדרך כלל:
- ה-stack כבר מסוכן
- הקצאה נוספת מסוכנת בגלל השחתת heap
- המתנה נעצרת בגלל נעילה שהוחזקה בזמן החריגה
- אובייקטים שה-logger עצמו תלוי בהם כבר שבורים
לכן בטוח יותר להתייחס ל-handler האחרון לא כ”מקום שאפשר לעשות בו הכול”, אלא כ”מקום שאפשר לעשות בו מעט מאוד”.
flowchart TB
accTitle: ב-handler האחרון אפשר לעשות מעט
accDescr: תרשים המראה שה-hook של חריגה בלתי מטופלת עלול לפעול בהקשר של thread שבור, כשה-stack וה-heap מסוכנים, המתנה לנעילה עלולה לעצור, וגם התלויות של ה-logger שבורות, ולכן זה מקום שאפשר לעשות בו מעט מאוד.
c1["פועל בהקשר thread שבור"] --> c2["stack ו-heap מסוכנים"]
c1 --> c3["המתנה לנעילה עלולה לעצור"]
c1 --> c4["תלויות ה-logger שבורות"]
c2 --> c5["מקום שאפשר לעשות בו מעט מאוד"]
c3 --> c5
c4 --> c5
איור 3: ה-handler האחרון הוא לא “מקום שאפשר לעשות בו הכול” אלא מקום מוגבל מאוד.
2.2 fast fail וחריגות של מצב שבור מניחות “פעולה מזערית בתוך התהליך”
במצב של השחתת זיכרון או מצב קטלני, עדיף שלא לצפות מטיפול חריגות רגיל.
בפרט, __fastfail בצד native וחריגות שחשודות במצב שבור מתוכננים לכיוון “סיום מיידי עם overhead מינימלי ככל האפשר”.
כלומר, הגישה הטבעית היא: אם היומן האחרון ב-in-process נכתב - מזל, והראיה העיקרית נמצאת בצד מערכת ההפעלה / תהליך נפרד.
2.3 גם אירוע החריגה הבלתי מטופלת ב-.NET אינו מקום ל”עיבוד התאוששות כבד”
AppDomain.UnhandledException של .NET נוח, אבל עדיף לחשוב שמה שמותר לעשות כאן הוא רק רישום קצר.
- עלול להיות מושפע מנעילה שהוחזקה בזמן החריגה
- לא כל חריגה, כולל חריגת מצב שבור, ניתנת לתפיסה בטוחה
- אם כופים כאן מדיניות המשך, נוטים להאריך חיים במצב חצי-שבור
ריאלי לתפוס את זה כ“אירוע חריגה בלתי מטופלת = ההתראה האחרונה”, ולא כ“נקודת התאוששות בטוחה”.
flowchart TB
accTitle: אירוע החריגה הבלתי מטופלת הוא התראה אחרונה
accDescr: תרשים המראה שב-AppDomain.UnhandledException מותר רק רישום קצר, לא כל חריגה ניתנת לתפיסה בטוחה, ושהאירוע הוא ההתראה האחרונה ולא נקודת התאוששות בטוחה.
d1["אירוע חריגה בלתי מטופלת"] --> d2["מותר רק רישום קצר"]
d2 --> d3["משמש כהתראה אחרונה"]
d1 -.-> d4["אינו נקודת התאוששות בטוחה"]
d4 -.-> d5["מדיניות המשך כפויה - הארכת חיים חצי-שבורה"]
איור 4: אירוע החריגה הבלתי מטופלת הוא שער לרישום, לא מקום להתאוששות.
3. ארכיטקטורה מומלצת - הפרדה בין רגע הקריסה להפעלה הבאה
הכי קל לסדר בשיטה שמפרידה בין מה עושים ברגע הקריסה למה עושים אחרי ההפעלה מחדש.
קודם, תרשים אחד שמראה עבור כל אחת משלוש השכבות באחריות איזה תהליך, ולאן היא נופלת.
flowchart TD
accTitle: מפת שלוש השכבות של ראיות הקריסה
accDescr: תרשים המראה שהתהליך של האפליקציה כותב יומן רגיל וסמן קריסה סופי, WER LocalDumps שומר dump מחוץ לתהליך, תהליך watchdog רושם exit code והחלטת הפעלה מחדש, וכולם נופלים לתיקייה מקומית קבועה שממנה מתבצע עיבוד אחרי ההפעלה הבאה.
subgraph APP["תהליך האפליקציה"]
L1["יומן רגיל - כרונולוגיה append-only"]
L2["סמן קריסה סופי - כותב שורה אחת ומסיים"]
end
subgraph WIN["צד Windows"]
WER["WER LocalDumps - שומר dump מחוץ לתהליך"]
end
subgraph WD["תהליך watchdog"]
EX["רושם exit code וזמן סיום - מחליט אם להפעיל מחדש"]
end
subgraph NEXT["התהליך הבריא שהופעל הבא"]
POST["דחיסה / העלאה / התראה - מזהה סיום חריג קודם"]
end
DISK[("תיקייה מקומית קבועה")]
L1 --> DISK
L2 --> DISK
WER --> DISK
EX --> DISK
DISK --> POST
L1 -. חריגה .-> L2
L2 -. סיום תהליך .-> WER
L2 -. סיום תהליך .-> EX
איור 5: התמונה המלאה של שלוש שכבות הראיות. התהליך הקורס כותב רק שתיים, והראיה העיקרית נמצאת מחוצה לו.
שלוש נקודות מרכזיות:
- התהליך הקורס כותב בעצמו רק שני דברים - שנמצאים בתוך המסגרת “תהליך האפליקציה”. וגם סמן הקריסה הסופי מסתכם ב”כתיבת שורה אחת וסיום”.
- הראיה העיקרית נמצאת מחוץ לתהליך. ה-dump של WER ורישום ה-watchdog נשארים גם כשהאפליקציה שבורה.
- המפתח להצלבה הוא session ID ו-PID משותפים. בלעדיהם, שלוש הראיות נראות כשלושה מקרים נפרדים.
| שלב | מטרה | היכן פועל | מה עושים |
|---|---|---|---|
| זמן רגיל | לשמור כרונולוגיה | בתוך האפליקציה | יומן מובנה, heartbeat, אירועי גבול |
| ברגע הקריסה | להפיל ראיה מינימלית | בתוך האפליקציה + OS | סמן קריסה סופי, dump של WER |
| מיד אחרי הסיום | לזהות exit בלתי צפוי | תהליך נפרד | רישום exit code, החלטת הפעלה מחדש, התראה |
| אחרי ההפעלה הבאה | עיבוד כבד | תהליך בריא חדש | דחיסה, העלאה, התראת משתמש, ניקוי יומנים ישנים |
החלוקה הזו מייצבת מאוד את התכנון.
3.1 תצורה מינימלית
בכלי עסקי קטן או WPF/WinForms פנים-ארגוני, לרוב מספיק בערך זה:
- יומן רגיל: קובץ append-only מקומי
- סמן קריסה סופי: קובץ קצר ייעודי
- dump: WER LocalDumps
- בהפעלה הבאה: הצגת “הסתיים בפעם הקודמת בצורה חריגה. יש מידע לאבחון”
3.2 תצורה חזקה יותר
כדאי לחזק שלב אחד כשיש דרישות כמו:
- הפעלה 24/7
- בקרת ציוד, ניטור, פעולה ברקע
- הרבה COM/P/Invoke/SDK נייטיבי
- יש תהליכי בת, plugins, הרצת סקריפט
- בסביבת הלקוח לא מקובל “להישאר תקוע”
במקרה כזה, החלוקה
- תהליך worker: העיבוד המרכזי
- launcher/watchdog/service: ניטור הפעלה, רישום exit, הפעלה מחדש
- WER LocalDumps: בצד ה-worker
- ההפעלה הבאה או ה-watchdog: איסוף מידע לאבחון
הופכת מעשית מאוד.
flowchart TB
accTitle: חלוקת התפקידים בתצורה חזקה
accDescr: תרשים המראה שבדרישות חזקות כמו 24/7 ובקרת ציוד, מחלקים לתהליך worker שמבצע את העיבוד המרכזי ולתהליך watchdog שמנטר הפעלה ורושם exit ומפעיל מחדש, כשWER LocalDumps מוגדר בצד ה-worker ומידע האבחון נאסף בהפעלה הבאה או על ידי watchdog.
e0["דרישות חזקות(24/7, בקרת ציוד)"] --> e1["worker: העיבוד המרכזי"]
e0 --> e2["watchdog: ניטור, רישום exit, הפעלה מחדש"]
e1 -.-> e3["WER LocalDumps בצד ה-worker"]
e2 -.-> e4["איסוף אבחון בהפעלה הבאה או ב-watchdog"]
איור 6: בתצורה חזקה, מפרידים את הגוף הראשי מהניטור לתהליכים נפרדים עם תפקידים קבועים.
4. שיטות עבודה מומלצות ליומן הרגיל
אם מנסים להילחם רק עם השורה האחרונה ברגע הקריסה, לרוב מפסידים. מה שבאמת עוזר הוא היומן הרגיל עד לרגע האחרון.
4.1 יומן צריך “מידע שאפשר להצליב בדיעבד” ולא “טקסט לבני אדם”
רשימת הפריטים שכדאי לכלול ביומן הרגיל, לכל הפחות.
- timestamp ב-UTC
- זמן שחלף מתחילת התהליך
- PID/TID
- שם האפליקציה, גרסה, מספר build, מזהה commit
- session ID
- מזהה פעולה / מזהה job / מזהה הצלבה
- שם מודול / שם מסך / שם worker
- הפעולה החיצונית האחרונה
- כתיבה לקובץ
- עדכון DB
- שליחת פקודה לציוד
- בקשת תקשורת
- סוג החריגה, HRESULT/שגיאת Win32/קוד חריגה
- תקציר פרמטרי הקלט העיקריים
- מזהה יעד, בטווח שלא כולל מידע סודי
מומלץ פורמט JSON Lines של אירוע אחד לשורה, או פורמט key=value.
ב-JSON Lines, אירוע אחד נראה בערך כך.
{"ts":"2026-03-18T01:15:33.412Z","up_ms":184213,"pid":1234,"tid":9,"app":"MyApp","ver":"3.2.1.884","commit":"9f1c2ab","session":"4f1c","level":"Warning","module":"DeviceWorker","op":"JOB-20260318-0042","event":"ExternalCommandSent","target":"COM3","cmd":"SEQ_START","timeout_ms":3000}
זה ארוך, אבל מספיק לראות שורה אחת אחת כדי לדעת “מתי, איזה מופע הפעלה, איזו גרסה, באמצע איזו פעולה, מה עשה”. מתחבר לצד ה-dump דרך pid ו-session, ולצד ה-build דרך ver ו-commit.
בפורמט key=value, אותו תוכן נראה כך.
ts=2026-03-18T01:15:33.412Z up_ms=184213 pid=1234 tid=9 app=MyApp ver=3.2.1.884 commit=9f1c2ab session=4f1c level=Warning module=DeviceWorker op=JOB-20260318-0042 event=ExternalCommandSent target=COM3 cmd=SEQ_START timeout_ms=3000
יותר חשוב “שאפשר יהיה להצליב שלושה קבצים בדיעבד”, מאשר להשאיר טקסט ארוך לבני אדם.
flowchart TB
accTitle: שורה אחת מחברת שלוש ראיות
accDescr: תרשים המראה ששורה אחת ביומן הרגיל מתחברת ל-dump דרך pid ו-session, ולצד ה-build דרך ver ו-commit, ושהיכולת להצליב שלושה קבצים בדיעבד חשובה יותר מטקסט ארוך לבני אדם.
f1["שורה אחת ביומן הרגיל"] --> f2["pid/session אל צד ה-dump"]
f1 --> f3["ver/commit אל צד ה-build"]
f2 --> f4["ניתן להצליב שלושה קבצים"]
f3 --> f4
איור 7: שורה אחת ביומן מתחברת לראיות אחרות דרך pid, session, ver ו-commit.
4.2 שומרים אירועים קריטיים באופן סינכרוני
אם כל היומן הרגיל נכתב סינכרונית, זה כבד. אבל אם הכול מסתמך על buffer אסינכרוני, הכול נעלם יחד ברגע הקריסה.
לכן, ריאלי לשנות את הטיפול לפי הרמה בעבודה בפועל.
- אירועי
Informationדקים: מותר לאגור ב-buffer Warningומעלה: לעשות flush מוקדם- אירועי גבול חשובים: לשמור באופן סינכרוני
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
בקיצור, רק את גבולות העסק מפילים כמו שצריך לקרקע.
flowchart TB
accTitle: הטיפול משתנה לפי רמת האירוע
accDescr: תרשים המראה שאירועי Information דקים מותר לאגור ב-buffer, Warning ומעלה עושים flush מוקדם, ואירועי גבול עסקיים נשמרים באופן סינכרוני, וכך רק גבולות העסק מופלים לקרקע.
g0["כתיבת יומן"] --> g1["Information: מותר buffer"]
g0 --> g2["Warning ומעלה: flush מוקדם"]
g0 --> g3["אירוע גבול: שמירה סינכרונית"]
g3 -.-> g4["רק גבולות עסק מופלים לקרקע"]
איור 8: לא הכול סינכרוני ולא הכול ב-buffer - הטיפול משתנה לפי רמת האירוע.
4.3 מפרידים בין “היומן הרגיל שכותבים כרגע” ל”סמן הקריסה האחרון”
זה די חשוב.
אם מנסים לשים הכול ב-rolling log אחד, זה קורה:
- היה באמצע rotation
- נשאר בתור אסינכרוני
- ה-logger עצמו מת מיד אחרי החריגה
- שורת היומן נחתכה באמצע
לכן מומלץ לפצל לפחות לשני קבצים:
app-<session>.jsonlיומן כרונולוגי רגילfatal-last.logאוfatal-<session>.logייעודי לסמן הקריסה הסופי
עצם הבהירות “איפה נשמרת השורה האחרונה” עוזרת מאוד בשטח.
flowchart TB
accTitle: מפרידים יומן רגיל מסמן fatal
accDescr: תרשים המראה ששימוש ברישום אחד בלבד מסתכן באובדן השורה האחרונה עקב rotation, תור אסינכרוני או מות ה-logger, ולכן מפצלים לשני קבצים - יומן כרונולוגי רגיל וקובץ ייעודי לסמן הקריסה הסופי.
h1["הכול ב-rolling log אחד"] --> h2["נעלם ב-rotation / תור / מות logger"]
h2 -.->|"במקום זאת"| h3["מפצלים ליומן רגיל וסמן fatal"]
h3 --> h4["ברור איפה נשמרת השורה האחרונה"]
איור 9: מקום השורה האחרונה מקובע בקובץ נפרד מהיומן הרגיל.
4.4 יעד שמירת היומן קבוע ומקומי, לא ברשת
בזמן קריסה, מסוכן להסתמך על נתיב UNC, NAS, HTTP או API בענן.
כי מעורבים:
- ניתוק רגעי ברשת
- עיכוב DNS
- פקיעת אישורי גישה
- המתנה ב-thread של ה-UI
- חוסר הרשאות בחשבון שירות
בקריסה, נופלים קודם לנתיב מקומי קבוע. השליחה מתבצעת בהפעלה הבאה או בתהליך נפרד.
4.5 שמים session בשם הקובץ
תאריך בלבד לא מספיק. כי מפעילים מחדש כמה פעמים באותו יום.
לדוגמה, בצורה כזו.
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
רק הבהירות “על איזה מופע הפעלה מדובר” משנה מאוד את מהירות הניתוח.
5. שיטות עבודה מומלצות לסמן הקריסה הסופי
זה לא המקום לבנות logger מלא. זה מקום לכתיבה חד-פעמית, קצרה, שנוטה להיות ודאית.
5.1 המטרה היא “לקבע שער כניסה” ולא “פרטי הסיבה”
עדיף לצמצם את המידע שנכנס לסמן הקריסה הסופי.
- UTC של הקרות האירוע
- PID/TID
- session ID
- גרסה / מספר build
- מאיזה hook זה הגיע
- AppDomain.UnhandledException
- Application.ThreadException
- DispatcherUnhandledException
- SetUnhandledExceptionFilter
- _set_invalid_parameter_handler
- set_terminate
- סוג החריגה או קוד החריגה
- הודעה קצרה, אם אפשר
- מזהה הפעולה האחרונה
- שם קובץ היומן הרגיל
- תיקיית ה-dump המשוערת
זה מספיק.
flowchart TB
accTitle: מטרת הסמן היא לקבע שער כניסה
accDescr: תרשים המראה שסמן הקריסה הסופי לא מכיל פרטי סיבה אלא מקבע שער כניסה לחקירה - איזה hook, סוג חריגה, session, שם יומן ותיקיית dump - כשפרטי הסיבה עצמם נמצאים ב-dump וביומן הרגיל.
i1["סמן קריסה סופי"] --> i2["hook, סוג חריגה, session"]
i1 --> i3["שם יומן רגיל ותיקיית dump"]
i2 --> i4["שער כניסה לחקירה מקובע"]
i3 --> i4
i4 -.-> i5["פרטי הסיבה - ב-dump וביומן הרגיל"]
איור 10: תפקיד הסמן הוא לא פרטי הסיבה, אלא לקבע את שער הכניסה לחקירה.
5.2 מה אסור לעשות ב-handler של קריסה
כל אחד מאלה נוטה מאוד להתפוצץ.
- לפתור logger מ-DI container
- להשתמש ב-async/await
- לזרוק Task
- להמתין לנעילה
- לבנות JSON מורכב
- לגעת באובייקט COM
- להציג דו-שיח UI
- לדחוס
- לשלוח HTTP/SMTP/Slack/Teams
- לנתח ולסכם dump
- לתפוס חריגה ולהמשיך
handler של קריסה אינו המשך של זרימת עיבוד רגילה. מטים אותו ל”כתיבה מקומית מזערית בלבד, ואז סיום”.
5.3 מה כן עושים ב-handler של קריסה
ולהפך, מה שכן עושים די פשוט.
- מונעים כניסה כפולה
- כותבים שורה אחת בלבד
- עושים flush
- מסיימים
בסדר הזה.
רצוי להשתמש ב-
- תיקייה ייעודית שנוצרה מראש
- נתיב שקיומו אומת מראש
- יעד שה-ACL שלו אומת
ביומן הרגיל, יותר מדי flush כבד, אבל סמן ה-fatal הוא מספר זעיר של פעמים, אז מותר כאן flush חזק.
ב-.NET זה FileStream.Flush(true), ב-native זה FlushFileBuffers - קל יותר לתכנן אם מתייחסים לזה כ“רק את השורה הזו מפילים לקרקע מיד”.
flowchart TB
accTitle: ארבעת שלבי handler הקריסה
accDescr: תרשים המראה שב-handler הקריסה עושים רק ארבעה שלבים - מונעים כניסה כפולה, כותבים שורה אחת, עושים flush, ומסיימים - ומכיוון שסמן ה-fatal נדיר, מותר flush חזק שמפיל לדיסק.
j1["1. מונעים כניסה כפולה"] --> j2["2. כותבים שורה אחת"]
j2 --> j3["3. עושים flush"]
j3 --> j4["4. מסיימים"]
j3 -.-> j5["השורה הזו מופלת לקרקע מיד"]
איור 11: ב-handler עושים רק ארבעה שלבים, בסדר הזה בלבד.
5.4 לא מנסים להמשיך
עבור חריגה בלתי צפויה שמקורה בבאג, בטוח יותר לחשוב על ה-handler האחרון כמכשיר רישום, לא מכשיר התאוששות.
בפרט, כדאי לבסס “לא ממשיכים” במקרים כמו:
- גם
NullReferenceExceptionאוInvalidOperationExceptionשקרו באמצע עדכון מצב משותף - חריגה בלתי צפויה ב-thread של ה-UI
- חריגה בלתי צפויה שדלפה מלולאת ניטור או לולאת אב
- AccessViolationException
- StackOverflowException
- חריגה בגבול native
- invalid parameter/purecall/terminate של ה-CRT
מובן הרצון “לא רוצים להפיל”, אבל לשרוד במצב חצי-שבור לרוב קשה יותר גם לאבחון וגם לתפעול.
בזמן הסיום, כדאי לשקול API של סיום מיידי - ב-.NET זה Environment.FailFast, ב-native זה RaiseFailFastException או __fastfail - ולא לצפות מ-finally או מניקיון רגיל.
flowchart TB
accTitle: מכשיר רישום, לא מכשיר התאוששות
accDescr: תרשים המראה שבחריגה בלתי צפויה שמקורה בבאג, שרידה במצב חצי-שבור קשה גם לאבחון וגם לתפעול, ולכן עדיף לרשום ולסיים בעזרת API של סיום מיידי כמו FailFast.
k1["חריגה שמקורה בבאג"] --> k2["שרידה במצב חצי-שבור"]
k1 --> k3["רישום וסיום"]
k2 -.-> k4["קשה לאבחון ולתפעול"]
k3 --> k5["לשקול FailFast וכדומה"]
איור 12: ה-handler האחרון הוא מכשיר רישום וסיום, לא מכשיר התאוששות.
5.5 מימוש מינימלי ב-C#
אם מורידים את המדיניות עד כה ישירות ל-C# מ-.NET 6 ואילך, מתקבלת כמות כזו. בלי DI ובלי logger.
using System;
using System.IO;
using System.Text;
using System.Threading;
internal static class FatalMarker
{
// נתיב מקומי קבוע שנוצר מראש ונבדקה בו כתיבה
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "Logs");
// המפתח להצלבה עם יומן רגיל, dump ורישום watchdog
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// דגל שמונע כניסה כפולה. 0 פירושו טרם נכתב
private static int _written;
/// <summary>קוראים לזה פעם אחת בלבד, מיד אחרי הפעלת האפליקציה.</summary>
public static void Install()
{
// לא רוצים לקרוא ל-CreateDirectory בזמן קריסה, אז יוצרים כאן מראש
Directory.CreateDirectory(LogDirectory);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
/// <summary>קוראים מהמקום שבו הוחלט ש"אסור להמשיך יותר".</summary>
public static void FailNow(string reason)
{
Write("FailNow", null, reason);
Environment.FailFast(reason);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Write("AppDomain.UnhandledException", e.ExceptionObject as Exception, null);
// לא מסיימים כאן. זו חריגה בלתי מטופלת, אז ה-CLR ימשיך אחר כך לטיפול הסיום המוגדר כברירת מחדל.
// אם קוראים כאן ל-FailFast, הסיבה שנשארת ב-dump של WER
// תוחלף מהחריגה המקורית ל-FailFast.
}
private static void Write(string hook, Exception ex, string note)
{
// מהפעם השנייה ואילך לא עושים כלום
if (Interlocked.Exchange(ref _written, 1) != 0)
{
return;
}
try
{
string fileName = string.Format(
"MyApp_fatal_{0:yyyyMMdd_HHmmss}_pid{1}_session-{2}.log",
DateTime.UtcNow, Environment.ProcessId, SessionId);
string path = Path.Combine(LogDirectory, fileName);
// בונים שורה אחת רק בשרשור מחרוזות, בלי לקרוא ל-serializer
string line = string.Join("\t",
"ts=" + DateTime.UtcNow.ToString("O"),
"pid=" + Environment.ProcessId,
"tid=" + Environment.CurrentManagedThreadId,
"session=" + SessionId,
"hook=" + hook,
"type=" + (ex != null ? ex.GetType().FullName : "none"),
"message=" + Flatten(ex != null ? ex.Message : note),
"log=" + LogDirectory);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
byte[] bytes = new UTF8Encoding(false).GetBytes(line + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
// העברת true מפילה עד לדיסק, לא רק ל-cache של המערכת
stream.Flush(true);
}
}
catch
{
// אם נכשל כאן, אין יותר מה לעשות. תופסים ומסיימים
}
}
private static string Flatten(string s)
{
return string.IsNullOrEmpty(s) ? string.Empty : s.Replace("\r", " ").Replace("\n", " ");
}
}
בצד הקורא, רק קוראים ל-Install() מיד אחרי ההפעלה. אם שוכחים את זה, הקוד למעלה לא פועל אפילו בייט אחד.
internal static class Program
{
private static void Main(string[] args)
{
FatalMarker.Install();
// מכאן ואילך, תהליך ההפעלה הרגיל של האפליקציה
RunApplication(args);
}
private static void RunApplication(string[] args)
{
// לדוגמה: אם הוחלט שהמצב המשותף שבור, מפילים בלי להמשיך
// FatalMarker.FailNow("device state inconsistent");
}
}
60 השורות האלה שומרות רק על ארבעת הדברים שהוזכרו בסעיף 5.3.
Interlocked.Exchangeמונע כניסה כפולה- שרשור מחרוזות כותב שורה אחת
Flush(true)מפיל לדיסק- מ-
UnhandledExceptionלא ממשיכים
Environment.FailFast כותב הודעה ליומן אירועי היישומים של Windows, מסיים מיד את התהליך, וכולל את התוכן גם בדיווח השגיאה. כלומר, גם FailFast עצמו מוסיף ראיה אחת. אבל כפי שצוין קודם, קריאה לו במסלול החריגה הבלתי מטופלת משנה את אופן הופעת ה-dump, אז בחרו בזהירות איפה לקרוא לו.
flowchart TB
accTitle: בוחרים איפה קוראים ל-FailFast
accDescr: תרשים המראה ש-Environment.FailFast כותב ליומן האירועים ומסיים מיד, מוסיף ראיה אחת, אך קריאה לו במסלול החריגה הבלתי מטופלת מחליפה את סיבת ה-dump מהחריגה המקורית ל-FailFast, ולכן בוחרים בזהירות היכן לקרוא לו.
l1["Environment.FailFast"] --> l2["כותב ליומן האירועים ומסיים מיד"]
l2 --> l3["מוסיף ראיה אחת"]
l1 -.-> l4["קריאה במסלול חריגה בלתי מטופלת"]
l4 -.-> l5["סיבת ה-dump מוחלפת ל-FailFast"]
איור 13: FailFast מוסיף ראיה, אבל קריאה לו בתוך חריגה בלתי מטופלת מחליפה את הסיבה.
6. נקודות זהירות לפי framework
6.1 משותף ל-.NET: AppDomain.CurrentDomain.UnhandledException
זה שימושי כהתראה אחרונה. עם זאת, נמנעים כאן מעיבוד התאוששות כבד.
השימוש הבסיסי פשוט:
- כותבים סמן קריסה סופי
- במידת הצורך, משאירים הודעה מזערית ב-Windows Event Log
- לא ממשיכים
- לא ממתינים ולא מנסים שוב כאן
UnhandledException נוח, אבל בטוח יותר לא להניח שאפשר להחזיר את האפליקציה למצב בריא כאן.
6.2 WinForms: Application.ThreadException
הקושי כאן הוא שהוא תופס חריגה בלתי מטופלת ב-thread של ה-UI, כך שאפשר להמשיך למראית עין.
לצורך הצגת שגיאה עסקית צפויה בדו-שיח, זה עוד סביר, אבל לא מתאים לשימוש כהמשך אחרי חריגה בלתי צפויה שמקורה בבאג.
אם עדיפות אמיתית היא חקירת הסיבה, בטוח יותר:
- לעשות רק רישום מזערי ב-
ThreadException - או להטות ל-
UnhandledExceptionMode.ThrowException - ואז לסיים את התהליך ולשמור dump ויומן
6.3 WPF: Application.DispatcherUnhandledException
דומה גם ב-WPF.
- רק חריגה ב-thread של ה-UI היא היעד המרכזי
- עם
Handled = trueאפשר להמשיך למראית עין - אבל מול חריגה שמקורה בבאג, זה נוטה ליצור פער בין מצב המסך למצב הפנימי
לכן, גם ב-WPF בטוח יותר לא להשתמש בזה כמכשיר הארכת חיים, אלא כשער לרישום.
flowchart TB
accTitle: לא מאריכים חיים דרך אירועי UI
accDescr: תרשים המראה ש-ThreadException של WinForms ו-DispatcherUnhandledException של WPF מאפשרים המשך למראית עין, אך מול חריגה שמקורה בבאג נוצר פער בין מצב המסך למצב הפנימי, ולכן משתמשים בהם כשער לרישום ומסיימים תוך שמירת dump ויומן.
m1["חריגה בלתי מטופלת ב-UI"] --> m2["המשך למראית עין"]
m2 -.-> m3["פער בין מסך למצב פנימי"]
m2 --> m4["משמש כשער לרישום"]
m4 --> m5["מסיימים ושומרים dump ויומן"]
איור 14: אירועי UI ב-WinForms/WPF משמשים שער לרישום, לא מכשיר הארכת חיים.
6.4 לא הופכים את TaskScheduler.UnobservedTaskException למסלול הראשי
זה לא “המבצר האחרון לפני הקריסה”.
אפשר להשתמש בו כעזר לזיהוי חריגות שהוחמצו ב-Task, אבל
הוא חלש כמסלול רישום ודאי ברגע הקריסה.
לכן, אפשר להשתמש בו למטרות כמו
- לגלות מוקדם חריגה שלא נצפתה
- לחשוף בזמן הפיתוח פערי תכנון ב-
Task
אבל עדיף לא להפוך אותו לכוכב של ה-handler האחרון.
6.5 native Win32/C++: לא לתת אמון מלא ב-SetUnhandledExceptionFilter
בצד native, קל לצפות יותר מדי מ-SetUnhandledExceptionFilter.
אבל הוא פועל בהקשר של ה-faulting thread, ולכן מושפע מ-
- stack לא תקין
- רקורסיה עמוקה
- heap שכבר שבור
- נעילה שהוחזקה בזמן החריגה
לכן, נכון להתייחס ל-SetUnhandledExceptionFilter כשער best effort לקבלת ההתראה האחרונה.
6.6 ב-C++ נייטיבי, קולטים גם את מסלולי הסיום של ה-CRT
ב-C++ נייטיבי, אם מסתכלים רק על SEH בלתי מטופל, יש פספוסים.
באופן קונקרטי, כדאי לבדוק גם:
-
_set_invalid_parameter_handler -
_set_purecall_handler -
set_terminate
הקבוצה הזו קולטת “מסלולי סיום” שמקורם ב-C runtime או C++ runtime.
בעבודה בפועל, סביר:
- לכתוב סמן קריסה סופי גם ב-handler-ים האלה
- אבל לא לעשות עיבוד התאוששות כבד
- לוודא סיום
- להשאיר את הראיה העיקרית ל-WER/dump
flowchart TB
accTitle: SEH בלבד יוצר פספוסים
accDescr: תרשים המראה שב-C++ נייטיבי, הסתכלות רק על SetUnhandledExceptionFilter מפספסת את מסלולי הסיום של ה-CRT, ולכן קולטים גם invalid parameter, purecall ו-terminate, כותבים רישום מזערי ומוודאים סיום, כשהראיה העיקרית ב-WER/dump.
n1["SetUnhandledExceptionFilter בלבד"] --> n2["פספוס מסלולי סיום CRT"]
n2 --> n3["קולטים גם invalid parameter/purecall/terminate"]
n3 --> n4["רישום מזערי וסיום ודאי"]
n4 -.-> n5["הראיה העיקרית - WER/dump"]
איור 15: רק SEH לא מספיק - קולטים גם את מסלולי הסיום של ה-CRT כדי לא לפספס.
7. WER LocalDumps כתשתית
7.1 ההמלצה הראשונה: WER LocalDumps
במובן של “אחרי הקריסה, להשאיר ראיה מינימלית באופן ודאי יחסית”, WER LocalDumps הכי נוח לטפל בו.
הסיבה פשוטה:
- אפשר לשמור dump בצד מערכת ההפעלה
- קל להכניס בלי כלים נוספים
- אפשר להגדיר ברמת אפליקציה
- אפשר להוציא את הראיה העיקרית מחוץ ל-in-process
מה שהיומן לבדו לא מגלה -
- איזה thread קרס
- באיזה stack
- באיזה גבול מודול
- איפה החשד - managed, native, COM, SDK
אפשר לראות בדיעבד, וזה חזק.
flowchart TB
accTitle: למה WER LocalDumps חזק
accDescr: תרשים המראה ש-WER LocalDumps שומר dump בצד מערכת ההפעלה, ניתן להגדרה ברמת אפליקציה, מוציא את הראיה העיקרית אל מחוץ ל-in-process, ומאפשר לראות בדיעבד thread, stack וגבול מודול.
p1["WER LocalDumps"] --> p2["שומר dump בצד ה-OS"]
p1 --> p3["ניתן להגדרה ברמת אפליקציה"]
p2 --> p4["מוציא ראיה אל מחוץ ל-in-process"]
p3 --> p4
p4 -.-> p5["רואים thread, stack וגבול מודול"]
איור 16: WER LocalDumps הוא תשתית שמוציאה את הראיה העיקרית מחוץ לתהליך הקורס.
7.2 הגדרה טיפוסית
לדוגמה, כדי לשמור dump של MyApp.exe בתיקייה C:\CrashDumps\MyApp, אפשר כך.
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
בהתחלה מספיקה החלטה בערך ברמה הזו.
| ערך | המלצה ראשונית |
|---|---|
DumpFolder |
תיקייה ייעודית |
DumpCount |
5-10 |
DumpType |
2 במחשב פיתוח; בשטח 1 או 2 לפי נפח ודרישות סודיות |
7.3 חובה לבדוק את ACL של יעד שמירת ה-dump
נכון גם ליומן וגם ל-dump - אין טעם להגדיר תיקייה שלא ניתן לכתוב אליה.
בפרט, ב-
- שירותי Windows
- תהליכי בת עם הפרדת הרשאות
- חשבון מוגבל במחשב שטח
- מעורבות UAC
ACL של יעד השמירה הוא הגורם המרכזי ל”ירי בלק”.
בודקים את יעד השמירה עד ל-
- יצירה מראש
- בדיקת כתיבה
- הגבלת כמות שמורה
- האם זה מקום שהצוות התפעולי יכול לגשת אליו
flowchart TB
accTitle: מונעים ירי בלק ב-ACL של יעד השמירה
accDescr: תרשים המראה ששירותי Windows וחשבונות מוגבלים מגדירים בטעות תיקייה שאי אפשר לכתוב אליה, ה-dump יורה בלק, ומונעים זאת ביצירה מראש, בדיקת כתיבה, הגבלת כמות ובדיקה שהמקום נגיש לצוות התפעולי.
q1["שירות / חשבון מוגבל"] --> q2["הגדרת תיקייה שלא ניתן לכתוב"]
q2 --> q3["ה-dump יורה בלק"]
q3 -.->|"למניעה"| q4["יצירה מראש ובדיקת כתיבה"]
q4 --> q5["גם הגבלת כמות ונגישות לצוות"]
איור 17: הגורם המרכזי לירי בלק של dump הוא ACL, ובודקים כתיבה מראש כדי למנוע.
7.4 כשרוצים לצרף את היומן הנוכחי לדוח WER
אם משתמשים בדיווח WER ל-Microsoft או בתפעול WER עצמאי, יש גם אפשרות לרשום דרך WerRegisterFile כדי לכלול את קובץ היומן הנוכחי בדיווח השגיאה.
עם זאת, בטוח יותר להתייחס לזה כמסלול נוסף, לא כתחליף לשמירה מקומית. כי מה שבאמת רוצים ברגע הקריסה הוא קודם כול שמירה מקומית ודאית יחסית במכשיר עצמו.
מבחינת הסדר, זה מעשי יותר:
- יומן רגיל מקומי
- סמן fatal מקומי
- dump מקומי
- במידת הצורך, רישום קבצים קשורים גם במסלול השליחה של WER
7.5 שומרים גם ניהול גרסאות, לא רק dump
גם אם לוקחים dump, אם בדיעבד -
- אין את ה-EXE/DLL של אותו זמן
- אין PDB
- לא ברור באיזה commit נבנה
זה נחלש מאוד.
לפחות אלה חייבים להישמר:
- הבינארי שהופץ
- ה-PDB המתאים
- גרסה
- זמן ה-build
- מזהה commit
- גרסת המתקין
איסוף dump ושמירת PDB הם חבילה אחת.
flowchart TB
accTitle: איסוף dump ושמירת PDB - חבילה אחת
accDescr: תרשים המראה שאם שומרים dump בלבד בלי PDB והבינארי מאותו זמן, לא ניתן לקרוא אותו בדיעבד, ולכן שומרים גם את הבינארי המופץ, ה-PDB, הגרסה ומזהה ה-commit יחד עם איסוף ה-dump.
r1["שומרים dump בלבד"] --> r2["אין PDB או בינארי מאותו זמן"]
r2 --> r3["אי אפשר לקרוא בדיעבד"]
r3 -.->|"לכן"| r4["שומרים בינארי מופץ ו-PDB"]
r4 --> r5["שומרים גם גרסה ומזהה commit"]
איור 18: אפשר לקרוא dump רק כשה-PDB והבינארי מאותה גרסה נשמרו יחד איתו.
7.6 עד לפתיחה הראשונה של dump שנתפס
”יש הרבה dump-ים באוסף, אבל אף אחד לא פתח אותם” - זה קורה ממש הרבה. את ההעמקה נשאיר למאמר ייעודי, כאן רק 10 הדקות הראשונות.
ההכנה כוללת שני דברים.
- לרכז לתוך תיקייה אחת PDB ובינארי מופץ מאותו build כמו ה-dump
- להכין את WinDbg מתוך Debugging Tools for Windows ב-Windows SDK
לאחר מכן, פותחים את ה-.dmp ב-WinDbg ומקלידים בזה אחר זה.
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;C:\myapp\pdb
.reload /f
.ecxr
!analyze -v
~*k
lm v
התפקיד של כל אחת:
| פקודה | מה עושה |
|---|---|
.sympath |
קובעת יעד חיפוש לסמלים. מציינים גם את שרת הסמלים של Microsoft וגם את מיקום ה-PDB שלכם |
.reload /f |
טוענת מחדש את הסמלים בכפייה |
.ecxr |
עוברת להקשר ה-register של רגע החריגה. אם שוכחים את זה, רואים את ה-stack של ההמתנה בצד WER, לא את מקום הקריסה |
!analyze -v |
מנתחת אוטומטית את החריגה הנוכחית ומציגה פירוט. קוראים את זה קודם |
~*k |
מציגה call stack של כל ה-threads. רואים מה עשו ה-threads שלא קרסו |
lm v |
מציגה את המודולים הטעונים והגרסאות שלהם. משמש להצלבה עם ההפצה ומספר ה-build |
אם עד לשלב הזה “לא מתקבל שם פונקציה” או “לא מתקבל מספר שורה”, הסיבה לרוב היא ש-PDB ממתאים ל-build אחר. חזרו לניהול הגרסאות בסעיף 7.5.
flowchart TB
accTitle: עשר הדקות הראשונות בפתיחת dump
accDescr: תרשים המראה שמרכזים PDB ובינארי מאותו build, פותחים את ה-dump ב-WinDbg, עוברים להקשר החריגה וקוראים ניתוח, ואם לא מתקבל שם פונקציה, זה סימן ל-PDB מגרסה אחרת וחוזרים לניהול גרסאות.
s1["מרכזים PDB ובינארי מאותו build"] --> s2["פותחים dump ב-WinDbg"]
s2 --> s3["עוברים להקשר החריגה, קוראים ניתוח"]
s3 -.-> s4["בלי .ecxr - רואים stack המתנה של WER"]
s3 --> s5{"מתקבל שם פונקציה ומספר שורה"}
s5 -->|"לא"| s6["PDB מ-build אחר, חזרה לניהול גרסאות"]
איור 19: פתיחת dump מתחילה בהכנה, פתיחה ומעבר להקשר החריגה, בסדר הזה.
דיון מעמיק יותר באיסוף וניתוח מרוכז במאמר הקשור בסוף.
8. גישה לשימוש ב-MiniDumpWriteDump או ב-crash reporter עצמאי
יש מצבים שדורשים מימוש עצמאי.
- רוצים כפתור “שמור מידע לאבחון” מה-UI
- רוצים לצרף גם יומן וגם קבצי הגדרות
- רוצים לטפל בקבוצת תהליכי בת יחד
- רוצים להוסיף מיסוך עצמאי לפני העלאה אוטומטית
עם זאת, הכי חשוב כאן הוא לא להעמיס יותר מדי גם את פעולת לקיחת ה-dump על הצד הקורס.
8.1 תהליך נפרד עדיף על self-dump
MiniDumpWriteDump חזק, אבל
בטוח יותר לקרוא לו מתהליך נפרד, לא מתוך התהליך שקרס עצמו.
תצורה טיפוסית נראית כך:
- ה-worker הראשי מזהה חריגה
- אם אפשר, מודיע ל-helper דרך event או named pipe
- ה-helper לוקח dump של ה-worker
- ה-helper מצרף יומן
tailוקבצי הגדרות - ה-helper שם באחר הסיום בתור להעלאה
כך, גם אם ה-worker שבור, צד ה-helper עדיין בריא.
sequenceDiagram
accTitle: לקיחת dump על ידי helper בתהליך נפרד
accDescr: תרשים המראה שכשה-worker הראשי מזהה חריגה, הוא מודיע ל-helper דרך event או named pipe, וה-helper הבריא לוקח dump של ה-worker, מצרף יומן וקבצי הגדרות, ושם בתור להעלאה אחרי הסיום.
participant W as worker ראשי
participant H as helper
W->>H: מודיע על חריגה דרך event/pipe
H->>W: לוקח dump של ה-worker
H->>H: מצרף יומן tail וקבצי הגדרות
H->>H: שם בתור להעלאה אחרי הסיום
Note over H: גם אם ה-worker שבור, ה-helper בריא
איור 20: את לקיחת ה-dump מבצע לא הצד הקורס, אלא תהליך helper בריא.
8.2 אם חייבים in-process, מטים לתוך thread ייעודי
גם כשאי אפשר להפריד לתהליך נפרד, עדיף לייעד thread נפרד ל-dump בלבד.
עם זאת, גם ככה זה נשאר במהותו best effort. לא הופך ל”100% בטוח כי הוספנו מימוש dump עצמאי”.
8.3 דברים כבדים עוברים להפעלה הבאה
יש דברים שקל להתפתות לעשות ב-reporter עצמאי.
- דחיסת zip
- הצלבה עם מידע symbol
- העלאה לשרת
- לכידת מסך
- שליפת מידע נוסף מ-DB
את אלה מעבירים לא לרגע הקריסה, אלא להפעלה הבאה או לצד ה-helper.
9. מה משתנה כשמוסיפים תהליך ניטור
במערכות שפועלות זמן ארוך, תהליך ניטור מאוד יעיל.
9.1 מה תהליך הניטור שומר
בצד watchdog/launcher/שירות אב, אפשר לשמור מידע כמו:
- זמן הפעלת תהליך הבת
- ארגומנטים בהפעלה
- PID
- גרסת היעד המנוטר
- זמן קבלת ה-heartbeat האחרון
- זמן הסיום
- exit code
- מספר הפעלות מחדש
- קיום dump או לא
- האם הופעל מחדש
רק בזכות זה, אפשר לראות בבירור
- האם באמת קרס
- האם זה היה כיבוי מערכת ההפעלה
- האם המשתמש סגר
- האם נהרג אחרי hang
- כמה פעמים היה לולאת הפעלה מחדש
flowchart TB
accTitle: מה אפשר להבחין מרישום ה-watchdog
accDescr: תרשים המראה שכשתהליך הניטור שומר exit code וזמן סיום, קבלת heartbeat אחרונה, ומספר הפעלות מחדש, אפשר להבחין מבחוץ בין קריסה אמיתית, כיבוי מערכת, סיום משתמש, או לולאת הפעלה מחדש.
t1["exit code וזמן סיום"] --> t4["ניתן להבחין מבחוץ"]
t2["heartbeat אחרון"] --> t4
t3["מספר הפעלות מחדש"] --> t4
t4 -.-> t5["קריסה, כיבוי מערכת, או סיום משתמש"]
איור 21: רישום ה-watchdog מאפשר להבחין מבחוץ בין סוגי הסיום.
9.2 מקרים שמתאימים במיוחד
כדאי לשקול ברצינות הפרדה במקרים כמו:
- worker שנושא SDK של ספק
- עיבוד תמונה/וידאו, device I/O
- לולאת אב לניטור או polling
- הרצת סקריפט או plugin
- host לנכסי COM/ActiveX קיימים
- גשר או שיתוף פעולה בין 64bit ל-32bit
כליאת עיבוד מסוכן ב-worker אחד מקלה גם על תכנון היומן וגם על תכנון ההתאוששות.
10. טעויות נפוצות
כאן מרוכזות מלכודות ברמת התכנון. הנושא ברמת ההליך - “מה אסור לעשות בתוך handler הקריסה” - מרוכז בסעיף 5.2, אז מי שבתהליך המימוש כדאי שיפנה לשם. פריטים שנראים חופפים (כמו שליחת HTTP בסעיף 10.3) - סעיף 5.2 כותב “למה זה בעייתי בתוך ה-handler”, וכאן כתוב “מה קורה בתפעול כתוצאה מכך”.
10.1 catch (Exception) ורק מדפיסים ליומן וממשיכים
הכי נפוץ, והכי מסוכן.
- שינוי חלקי נשאר
- מצב משותף נשבר
- תקלות המשך מתרבות
- נקודת הסיבה האמיתית מיטשטשת
במקום להוסיף שורת יומן אחת, לרוב התקלה מתארכת.
10.2 מסתמכים רק על תור של async logger
async logging כשלעצמו לא רע. הבעיה היא שגם ב-fatal path נערם לאותו תור ונגמר שם.
אם ה-worker נעצר ברגע הקריסה, כל התור נעלם איתו.
בטוח יותר להחזיק דרך מילוט שבה רק ה-fatal path כותב ישירות.
10.3 שולחים HTTP מתוך handler הקריסה
מפתה לממש, אבל זה די מסוכן.
- DNS
- TLS
- proxy
- אימות
- timeout
- המתנה לשליחה חוזרת
כל אלה רוכבים על ההקשר הקורס.
השליחה מתבצעת אחרי ההפעלה מחדש.
10.4 יש dump, אבל הוא לא מתחבר ליומן הרגיל
זה נפוץ מאוד.
- אין session בשם קובץ ה-dump
- אין PID/session בצד היומן
- אין PID בצד ה-watchdog
- מספרי ה-build לא תואמים
התוצאה: שלוש הראיות נראות כמו שלושה מקרים נפרדים.
flowchart TB
accTitle: כשל של ראיות שלא מתחברות
accDescr: תרשים המראה שכשלשם קובץ ה-dump חסר session, ליומן חסרים PID/session, ומספרי ה-build לא תואמים, שלוש הראיות - dump, יומן ורישום watchdog - נראות כמקרים נפרדים.
u1["בשם ה-dump חסר session"] --> u4["שלוש הראיות נראות נפרדות"]
u2["ביומן חסר PID/session"] --> u4
u3["מספרי build לא תואמים"] --> u4
איור 22: כשמפתח ההצלבה חסר, ראיות שהיו יכולות להתחבר נשארות נפרדות.
10.5 מאריכים חיים דרך אירועי חריגה בלתי מטופלת ב-WinForms/WPF
למראית עין “זה מפסיק לקרוס”, אז בהתחלה זה משמח. אבל בפועל, זה נוטה ליצור מצב זומבי שבו
- רק המסך נשאר
- ה-worker כבר מת
- רק פעילות הכפתור נשארת
- לא ברור אם השמירה הצליחה
10.6 לא בודקים את מסלולי הסיום בצד native
אם מסתפקים רק ב-SetUnhandledExceptionFilter, מפספסים את
- invalid parameter
- purecall
- terminate
- fast fail
ב-C++ נייטיבי, כדאי להיות מודעים לא רק ל-SEH, אלא גם למסלולי הסיום בצד ה-CRT/C++ runtime.
11. רשימת בדיקה מינימלית להטמעה
אם עומדים בכל אלה, זה כבר מוכן קרבית.
- היומן הרגיל נשמר שורה אחת לאירוע
- בכל שורות היומן יש UTC, PID, TID, version, session
- ProcessStart ו-ProcessExit נשמרים
- אירועי גבול חשובים נעשה להם flush סינכרוני
- קיים קובץ ייעודי לסמן הקריסה הסופי
- ב-fatal path לא עוברים דרך async logger
- WER LocalDumps מוגדר ברמת אפליקציה
- ה-ACL של יעד שמירת ה-dump אומת
- PDB והבינארי המופץ נשמרים
- בהפעלה הבאה אפשר לזהות סיום חריג קודם
- דחיסה/העלאה/התראה מתבצעים אחרי הפעלה מחדש או בתהליך נפרד
- ב-C++ נייטיבי סודרו גם invalid parameter/purecall/terminate
- במכשיר בדיקה, בוצעה קריסה מכוונת ואומת שהראיה באמת נשארת
השורה האחרונה חשובה במיוחד. אין טעם רק בתכנון - חייבים לבצע “בדיקה שבאמת תופסת עד הסוף”.
12. עד כמה בודקים
הפריטים שכדאי לוודא, מרוכזים בטבלה.
| בדיקה | מה בודקים |
|---|---|
| חריגה בלתי מטופלת ב-managed | האם יומן רגיל, סמן fatal ו-dump נמצאים כולם |
| חריגה ב-thread של UI | האם מסלול האירוע ב-WinForms/WPF תואם לציפייה |
| חריגה ב-thread של worker | האם מגיע עד AppDomain.UnhandledException, והאם ה-watchdog מזהה |
| חריגה native | האם ה-dump של WER באמת נלקח |
| invalid parameter / terminate | האם נשאר רישום מזערי גם במסלול CRT/C++ runtime |
| הריגה כפויה | גם אם אי אפשר ב-in-process, האם צד ה-watchdog רושם exit בלתי צפוי |
| הפעלה מחדש | האם עובדים ההתראה, האיסוף וההעלאה בהפעלה הבאה |
חשוב לא לומר “אמור להופיע יומן כשהחריגה נזרקת”, אלא לוודא “בתנאי הזה, הקובץ הזה נשאר”.
13. סיכום
אם רוצים להשאיר מידע לחקירה גם כשיישום Windows קורס מחריגה שמקורה בבאג, הציר המחשבתי די פשוט.
- לא מסתמכים רק על התהליך הקורס
- מחלקים ליומן רגיל, סמן קריסה סופי, וראיה בצד OS/תהליך נפרד
- בקריסה, שומרים בקצרה ומקומית בלבד
- עיבוד כבד עובר להפעלה הבאה או לתהליך נפרד
- WER LocalDumps כתשתית
- בסיס: רישום וסיום, ולא המשך
בסופו של דבר, “לבנות תצורה שאפשר לעקוב אחריה גם בלי השורה האחרונה” חזק יותר מ”להתאמץ על השורה האחרונה”.
flowchart TB
accTitle: תצורה שלא תלויה בשורה האחרונה
accDescr: תרשים המראה שהמטרה היא תצורה שאפשר לעקוב אחריה גם בלי השורה האחרונה, כשסמן הקריסה עדיין נשמר בקצרה בקובץ נפרד, והראיה העיקרית היא ה-dump והיומן הרגיל.
v0["המטרה"] --> v2["תצורה שאפשר לעקוב אחריה גם בלי השורה האחרונה"]
v2 --> v3["הסמן נשמר בקצרה בקובץ נפרד"]
v2 --> v4["הראיה העיקרית: dump ויומן רגיל"]
איור 23: עדיף לבנות תצורה שלא תלויה בשורה האחרונה, מאשר להתאמץ עליה בלבד.
ובכל זאת רוצים את השורה האחרונה, אז שומרים את סמן הקריסה הסופי בקצרה בקובץ נפרד. והראיה העיקרית האמיתית נשענת על ה-dump של WER והיומן הרגיל עד הרגע האחרון. זו שיטה יציבה למדי בעבודה בפועל עם יישומי Windows.
מאמרים קשורים
- מבוא לאיסוף דאמפים בקריסה של Windows - WER/ProcDump/WinDbg
- טבלת החלטה: לסיים או להמשיך בעקבות חריגה בלתי צפויה
מקורות
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
- Microsoft Learn: FileStream.Flush(Boolean) https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.flush
- Microsoft Learn: !analyze (WinDbg) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Microsoft Learn: .ecxr (Display Exception Context Record) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr–display-exception-context-record-
- Microsoft Learn: Symbol path for Windows debuggers https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מבוא לאיסוף crash dump ב-Windows - WER, ProcDump, WinDbg
המאמר מסדר את אופן הבחירה והשימוש ב-WER LocalDumps, ProcDump, MiniDumpWriteDump ו-WinDbg, כולל נקודות תפעוליות, כדי לחקור קריסות באפליק...
טבלת החלטה: לסיים או להמשיך אחרי חריגה בלתי צפויה
המאמר מסדר מתי כדאי לסיים אפליקציה ומתי אפשר להמשיך לפעול אחרי חריגה בלתי צפויה, מנקודת המבט של השחתת מצב, תופעות לוואי חיצוניות, threads...
תשתית בדיקות למקרי קצה ב-Windows עם Application Verifier
מהו Application Verifier: מסודר כאן יחד עם בניית תשתית בדיקות למקרי קצה ב-Windows באמצעות Handles, Heaps, Low Resource Simulation ו-!h...
חקירת קריסה בהפעלה ממושכת של מצלמה תעשייתית — פרק דליפת ה-handle
איך בוחנים אפליקציית Windows שקורסת בפתאומיות אחרי הפעלה ממושכת: דרך מקרה של אפליקציית בקרת מצלמה תעשייתית, מסודר כאן איך למצוא דליפת han...
הסיבה לכך ששידור חוזר ב-TCP עוצר תקשורת עם מצלמה תעשייתית, ואיך מבודדים אותה
איך מבודדים עצירות של כמה שניות בתקשורת עם מצלמה תעשייתית שנגרמות משידור חוזר ב-TCP: אובדן מנות, RTO, חותמות הזמן של RFC1323 ונקודות הבדי...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
חקירת תקלות ואיתור גורמים
קריסה שמופיעה רק בסביבת הלקוח, סיום חריג בשיעור שחזור נמוך, וניתוח סיבה על ידי הצלבת dump ויומן, הם נושאים שמתאימים היטב לחקירת תקלות וניתוח סיבה.
פיתוח יישומי Windows
איך לתכנן יומן רגיל, WER ו-watchdog ב-WPF, WinForms, יישומים שרצים ברקע ושירותי Windows קשור ישירות לפיתוח יישומי Windows עצמו.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- האם היישום שקורס יכול להבטיח בעצמו שמירת יומן?
- לא. כשלוקחים בחשבון קריסת stack, השחתת זיכרון, fast fail, סיום כפוי וניתוק חשמל, היומן האחרון בתוך התהליך (in-process) הוא במהותו best effort. בעבודה בפועל, מחלקים לשלוש שכבות - יומן כרונולוגי בזמן רגיל, סמן קריסה סופי ברגע הנפילה, וראיות קריסה שנשמרות בצד מערכת ההפעלה או בתהליך נפרד - ובונים תצורה שלא מסתמכת רק על התהליך הקורס. הכי בטוח הוא שילוב של יומן רגיל + סמן קריסה סופי + WER LocalDumps.
- מה אסור לעשות בתוך handler של קריסה?
- כל עיבוד כבד. פתרון logger מ-DI container, async/await, המתנה לנעילה, בניית JSON מורכב, פעולה על אובייקט COM, דו-שיח UI, דחיסה, ושליחת HTTP/SMTP/Slack - כל אלה נוטים מאוד להתפוצץ. מה שכן עושים מצטמצם לארבעה: מונעים כניסה כפולה, כותבים שורה אחת בלבד, עושים flush, ומסיימים. עיבוד כבד כמו דחיסה, העלאה והתראה עוברים להפעלה הבאה או לתהליך נפרד.
- איך מגדירים את WER LocalDumps?
- מגדירים תחת HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\שם-האפליקציה.exe ברג'יסטרי את DumpFolder (תיקייה ייעודית), DumpCount (בסביבות 5-10), ו-DumpType (2 במחשב פיתוח, 1 או 2 בשטח לפי נפח ודרישות סודיות). חשוב לבדוק את ה-ACL של תיקיית היעד - הכשל האופייני הוא הגדרת תיקייה שאי אפשר לכתוב אליה משירות Windows או מחשבון מוגבל. בנוסף, איסוף dump ושמירת PDB והבינארי המופץ הם חבילה אחת - אם אחד מהם חסר, אי אפשר לקרוא בדיעבד.
- האם מותר לתפוס חריגה ולהמשיך עם DispatcherUnhandledException ב-WPF?
- מסוכן כשמדובר בחריגה בלתי צפויה שמקורה בבאג. עם Handled = true אפשר להמשיך למראית עין, אבל זה נוטה ליצור מצב זומבי - המסך נשאר אבל ה-worker כבר מת, כפתור נשאר פעיל אבל לא ברור אם השמירה הצליחה. בטוח יותר להשתמש באירוע החריגה הבלתי מטופלת כשער לרישום ולא כמנגנון התאוששות - לכתוב את סמן הקריסה הסופי ואז לסיים בלי להמשיך, ולהשאיר את הראיה העיקרית ל-dump של WER וליומן הרגיל עד לרגע הקריסה. לסיום כדאי לשקול API של סיום מיידי כמו Environment.FailFast.