تصميم يُبقي السجلات والـ dump عند انهيار تطبيق Windows
· آخر تحديث: · 小村 豪 · تطوير Windows, معالجة الاستثناءات, تسجيل, WER, crash dumps, تحقيق العلل
سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)
سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.
- أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240865)
- أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
- أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
- النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621420)
هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.
小村 豪 (2026). تصميم يُبقي السجلات والـ dump عند انهيار تطبيق Windows. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621420 https://comcomponent.com/ar/blog/2026/03/19/000-windows-app-crash-logging-best-practices/
- DOI (أحدث نسخة)
- 10.5281/zenodo.21621420
- DOI (هذه النسخة)
- 10.5281/zenodo.22279850
أكثر ما يشق في تحقيق أعطال تطبيقات Windows حالة نعرف أنه سقط، لكن سبب السقوط غير باقٍ.
خصوصاً في مشاريع كهذه تثقل المشكلة كثيراً.
- لا يسقط إلا في بيئة العميل
- لا يسقط إلا بعد تشغيل طويل
- WPF / WinForms / خدمة Windows / تطبيق مقيم، ومعدل التكرار منخفض
- يدخل فيه COM أو P/Invoke أو native DLL أو SDK من مورّد
- «نص الاستثناء وحده» مأخوذ لكن سياق ما قبله مباشرة غير موجود
لكن بصراحة من البداية، لا يمكن «ضمان» إبقاء سجل بالعملية المنهارة وحدها. إذا أدخلنا فساد المكدس وفساد الذاكرة وfast fail والإنهاء القسري وانقطاع الكهرباء، فآخر سجل داخل العملية best effort في جوهره.
flowchart TB
accTitle: آخر سجل داخل العملية best effort
accDescr: مخطّط يبيّن أنه إذا أدخلنا فساد المكدس وفساد الذاكرة وfast fail والإنهاء القسري وانقطاع الكهرباء، فلا يمكن ضمان إبقاء سجل بالعملية المنهارة وحدها، وآخر سجل داخل العملية best effort في جوهره.
a1["فساد مكدس وفساد ذاكرة"] --> a4["آخر سجل داخل العملية"]
a2["fast fail وإنهاء قسري"] --> a4
a3["انقطاع كهرباء"] --> a4
a4 --> a5["في جوهره best effort"]
a5 -.-> a6["«الإبقاء بيقين» غير ممكن"]
الشكل 1: العملية المنهارة وحدها لا تستطيع «ضمان» إبقاء آخر سجل.
ما ينبغي استهدافه في العمل هو تكوين لا يعتمد على داخل العملية المنهارة وحدها. أي نفكّر بثلاث طبقات:
- سجل زمني في الأوقات العادية
- علامة انهيار نهائية في لحظة السقوط
- أدلة انهيار يبقيها نظام التشغيل أو عملية أخرى
في هذه المقالة نرتّب، بافتراض تطبيقات سطح مكتب Windows وتطبيقات مقيمة وخدمات Windows وأدوات ربط أجهزة، أفضل ممارسات حتى لا تُفقد قابلية التحقيق حتى عند السقوط باستثناء ناجم عن خطأ برمجي.
1. الخلاصة أولاً
نرتّب الخلاصات أولاً.
- الأهم على الإطلاق: لا نراهن بـ «آخر سجل» على معالج واحد داخل العملية.
- الأكثر أماناً في العمل الجمع بين سجل عادي + علامة انهيار نهائية + WER LocalDumps.
- في تشغيل طويل أو ربط أجهزة أو إضافات أو اختلاط native SDK، إضافة عملية مراقبة (watchdog / launcher / service) تقوّي التصميم كثيراً.
- في معالج الانهيار القاعدة عدم القيام بمعالجة ثقيلة. نُخرج الضغط وإرسال HTTP وحل DI ومربعات واجهة وتوليد 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 تلتقط إنهاء التطبيق غير الطبيعي من جهة نظام التشغيل وتسجّله. إعداد إبقاء dump محلياً هو LocalDumps |
| dump / minidump | crash dump | ملف يحفظ محتوى ذاكرة العملية في لحظة السقوط. يمكن لاحقاً رؤية الخيوط والمكدس والوحدات |
| PDB | Program Database | ملف رموز يُولَّد عند البناء. بدونه حتى فتح الـ dump لا يُظهر أسماء الدوال أو أرقام الأسطر |
| in-process | داخل العملية | المعالجة داخل العملية نفسها التي تسقط. الضد عملية أخرى |
| best effort | أفضل جهد | صفة «يبقى إن نجح، بلا ضمان». سجل داخل العملية في لحظة السقوط كذلك |
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 | معرّف جلسة | قيمة تميّز «عن أي نسخة تشغيل نتحدث». مفتاح مطابقة السجلات والـ dump وسجل watchdog |
في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 24، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle
2. لماذا لا يمكن «الضمان» داخل العملية وحدها
إن غامت هذه النقطة يتذبذب التصميم.
2.1 سياق الخيط الساقط نفسه قد يكون مكسوراً
خطاف الاستثناء غير المعالَج ومرشّح الاستثناء العلوي قد يعملان في سياق خيط الجهة المكسورة. في هذه اللحظة شائع أن:
- المكدس صار خطراً
- فساد الكومة يجعل التخصيص الإضافي خطراً
- الانتظار بسبب قفل كان ممسوكاً عند وقوع الاستثناء يوقف الأمر
- الكائن الذي يعتمد عليه logger نفسه مكسور سلفاً
لذا أأمن أن نرى آخر معالج لا كـ «مكان نستطيع فيه أي شيء» بل كـ «مكان ما يمكن فعله فيه قليل جداً».
flowchart TB
accTitle: ما يمكن فعله في آخر معالج قليل
accDescr: مخطّط يبيّن أن خطاف الاستثناء غير المعالَج قد يعمل في سياق خيط الجهة المكسورة، فالمكدس والكومة خطران، والانتظار على قفل قد يوقف، واعتماد logger قد يكون مكسوراً، لذا يُرى كمكان ما يمكن فعله فيه قليل جداً.
c1["يعمل في سياق خيط مكسور"] --> c2["المكدس والكومة خطران"]
c1 --> c3["قد يتوقف بانتظار قفل"]
c1 --> c4["اعتماد logger قد ينكسر أيضاً"]
c2 --> c5["مكان ما يمكن فعله فيه قليل جداً"]
c3 --> c5
c4 --> c5
الشكل 3: آخر معالج ليس «مكاناً نستطيع فيه أي شيء» بل مكاناً مليئاً بالقيود.
2.2 fast fail واستثناءات فساد الحالة تفترض «أقل عمل ممكن داخل العملية»
عند فساد الذاكرة أو حالة قاتلة، الأفضل ألا نتوقع معالجة الاستثناء العادية.
خصوصاً عائلة __fastfail في الجهة الأصلية والاستثناءات التي يُشتبه بفساد الحالة مصمَّمة باتجاه «إنهاء فوري بأقل عبء ممكن».
أي أن التفكير الطبيعي: آخر سجل داخل العملية محظوظ إن كُتب، والأدلة الرئيسة على جهة نظام التشغيل / عملية أخرى.
2.3 أحداث الاستثناء غير المعالَج في .NET ليست مكان «استعادة ثقيلة»
AppDomain.UnhandledException في .NET مريح، لكن الأفضل أن نرى ما يجوز هنا حتى تسجيل قصير.
- قد يتأثر بقفل كان ممسوكاً عند وقوع الاستثناء
- ليس أن كل استثناء فساد حالة يُؤخذ بأمان
- فرض سياسة مواصلة هنا يسهل إطالة حالة نصف مكسورة
الواقعي أن نمسك «حدث الاستثناء غير المعالَج = آخر إشعار» لا «نقطة استعادة آمنة».
flowchart TB
accTitle: حدث الاستثناء غير المعالَج آخر إشعار
accDescr: مخطّط يبيّن أن ما يجوز في AppDomain.UnhandledException تسجيل قصير حتى، وأنه ليس أن استثناءات فساد الحالة تُؤخذ بأمان كلها، فحدث الاستثناء غير المعالَج آخر إشعار لا نقطة استعادة آمنة.
d1["حدث استثناء غير معالَج"] --> d2["ما يجوز تسجيل قصير حتى"]
d2 --> d3["يُستخدم كآخر إشعار"]
d1 -.-> d4["ليس نقطة استعادة آمنة"]
d4 -.-> d5["فرض سياسة مواصلة يطيل حالة نصف مكسورة"]
الشكل 4: حدث الاستثناء غير المعالَج مدخل تسجيل لا مكان استعادة.
3. المعمارية الموصى بها - فصل crash-time عن after-restart
أسهل ترتيب هو فصل ما يُفعل عند الانهيار عن ما يُفعل بعد إعادة التشغيل.
نضع أولاً في ورقة واحدة مسؤولية أي عملية تسقط أين لكل طبقة من الثلاث.
flowchart TD
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، أحداث حدود |
| عند الانهيار | إسقاط أدلة حد أدنى | داخل التطبيق + نظام التشغيل | علامة انهيار نهائية، dump من WER |
| فور الإنهاء | كشف unexpected exit | عملية أخرى | تسجيل exit code، حكم إعادة التشغيل، إشعار |
| بعد التشغيل التالي | معالجة لاحقة ثقيلة | عملية سليمة جديدة | ضغط، رفع، إشعار مستخدم، ترتيب سجلات قديمة |
بهذا الفصل يثبت التصميم كثيراً.
3.1 التكوين الأصغر
لأداة أعمال صغيرة أو WPF / WinForms داخلي، غالباً يكفي نحو هذا أولاً.
- سجل عادي: ملف محلي append-only
- علامة انهيار نهائية: ملف قصير مخصص
- dump: WER LocalDumps
- عند التشغيل التالي: إخراج «انتهى التشغيل السابق على نحو غير طبيعي. توجد معلومات تشخيص»
3.2 تكوين أقوى
ما يستحق التقوية بدرجة عندما تكون المتطلبات كهذه.
- تشغيل 24/7
- تحكم بأجهزة، مراقبة، إقامة
- COM / P/Invoke / native SDK كثير
- عمليات ابن، إضافات، تنفيذ سكربت
- في بيئة العميل «التوقف الدائم» غير مسموح
عندها التقسيم إلى:
- عملية worker: المعالجة الرئيسة
- launcher / watchdog / service: مراقبة التشغيل، تسجيل exit، إعادة تشغيل
- WER LocalDumps: جهة worker
- التشغيل التالي أو watchdog: استرداد معلومات التشخيص
يصير موجّهاً جداً إلى العمل.
flowchart TB
accTitle: توزيع أدوار التكوين الأقوى
accDescr: مخطّط يبيّن أنه في تشغيل 24 ساعة أو تحكم بأجهزة نفصل عملية worker التي تتولى المعالجة الرئيسة عن launcher أو 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 السجل «معلومات يمكن مطابقتها لاحقاً» قبل «نص موجّه إلى إنسان»
نذكر بنوداً نريد إدخالها في السجل العادي كحد أدنى.
- طابع زمني UTC
- الزمن المنقضي منذ بدء العملية
- PID / TID
- اسم التطبيق، الإصدار، رقم البناء، معرّف الالتزام
- معرّف الجلسة
- معرّف تشغيل / معرّف وظيفة / معرّف ارتباط
- اسم الوحدة / اسم الشاشة / اسم العامل
- الفعل الخارجي السابق مباشرة
- كتابة ملف
- تحديث 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، وجهة البناء بـ 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 وبجهة البناء عبر ver و commit، فإمكان مطابقة ثلاثة ملفات لاحقاً أهم من نص طويل موجّه إلى إنسان.
f1["سطر السجل العادي"] --> f2["إلى جهة الـ dump عبر pid / session"]
f1 --> f3["إلى جهة البناء عبر ver / commit"]
f2 --> f4["يمكن مطابقة ثلاثة ملفات"]
f3 --> f4
الشكل 7: سطر السجل يرتبط بأدلة أخرى عبر pid و session و ver و commit.
4.2 أحداث حرجة تُبقى تزامنياً
جعل كل السجل العادي كتابة تزامنية يثقّل. لكن تسليم الكل إلى مخزن غير تزامني يجعله يختفي جملة في لحظة السقوط.
لذا في العمل واقعي تغيير المعالجة حسب المستوى.
- أحداث
Informationالدقيقة: يجوز التخزين المؤقت Warningفما فوق: flush مبكر- أحداث حدود مهمة: تُبقى تزامنياً
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
أي حدود العمل وحدها تُسقط إلى الأرض بصدق.
flowchart TB
accTitle: توزيع الكتابة حسب المستوى
accDescr: مخطّط يبيّن توزيع المعالجة حسب المستوى: أحداث Information الدقيقة يجوز تخزينها مؤقتاً، وWarning فما فوق flush مبكر، وأحداث حدود العمل تُبقى تزامنياً.
g0["كتابة السجل"] --> g1["Information: يجوز التخزين المؤقت"]
g0 --> g2["Warning فما فوق: flush مبكر"]
g0 --> g3["أحداث حدود: تُبقى تزامنياً"]
g3 -.-> g4["حدود العمل وحدها تُسقط إلى الأرض"]
الشكل 8: لا الكل تزامني ولا الكل مخزَّن مؤقتاً، بل نوزّع الكتابة حسب المستوى.
4.3 افصل «السجل العادي الجاري كتابته» عن «علامة الانهيار الأخيرة»
هذا مهم جداً.
محاولة إدخال الكل في rolling log واحد تؤدي إلى:
- كان أثناء التدوير
- بقي في طابور غير تزامني
- مات logger نفسه فور وقوع الاستثناء
- انقطع في وسط سطر السجل
لذا الموصى به الفصل إلى اثنين على الأقل.
app-<session>.jsonlسجل زمني عاديfatal-last.logأوfatal-<session>.logمخصص لعلامة الانهيار النهائية
مجرد وضوح «أين نُبقي آخر سطر» يساعد كثيراً في الميدان.
flowchart TB
accTitle: فصل السجل العادي عن علامة fatal
accDescr: مخطّط يبيّن أن إدخال الكل في rolling log واحد يختفي معه آخر سطر أثناء التدوير أو بقاء الطابور غير التزامني أو موت logger، لذا يُفصل إلى سجل زمني عادي وملف مخصص لعلامة الانهيار النهائية.
h1["إدخال الكل في rolling log واحد"] --> h2["يختفي أثناء التدوير أو بقاء الطابور أو موت logger"]
h2 -.->|"بدلاً من ذلك"| h3["الفصل إلى سجل عادي وعلامة fatal"]
h3 --> h4["يصير موضع آخر سطر واضحاً"]
الشكل 9: موضع آخر سطر يُثبَّت في ملف منفصل عن السجل العادي.
4.4 وجهة حفظ السجل محلية ثابتة، لا نستخدم وجهة شبكة
الاعتماد عند الانهيار على مسار UNC أو NAS أو HTTP أو واجهة سحابة خطر.
لأن يدخل فيه:
- انقطاع شبكة لحظي
- تأخير DNS
- انتهاء صلاحية بيانات اعتماد
- انتظار على خيط الواجهة
- نقص صلاحيات حساب الخدمة
عند الانهيار نُسقط أولاً إلى مسار محلي ثابت. الإرسال يكون بعد التشغيل التالي أو في عملية أخرى.
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
- معرّف الجلسة
- الإصدار / رقم البناء
- من أي خطاف جاءت
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- نوع الاستثناء أو رمزه
- رسالة بسيطة إن أمكن
- معرّف التشغيل السابق مباشرة
- اسم ملف السجل العادي
- المجلد المتوقع للـ dump
هذا يكفي.
flowchart TB
accTitle: غرض العلامة تثبيت المدخل
accDescr: مخطّط يبيّن أن علامة الانهيار النهائية ليست تفاصيل السبب، بل تضييق المعلومات لتثبيت مدخل التحقيق: من أي خطاف وبأي استثناء سقط، واسم ملف السجل العادي ومجلد dump المتوقع.
i1["علامة انهيار نهائية"] --> i2["أي خطاف ونوع استثناء وsession"]
i1 --> i3["اسم السجل العادي ومجلد dump"]
i2 --> i4["يُثبَّت مدخل التحقيق"]
i3 --> i4
i4 -.-> i5["تفاصيل السبب تُترك للـ dump والسجل العادي"]
الشكل 10: دور العلامة ليس تفاصيل السبب بل تثبيت مدخل التحقيق.
5.2 ما يُحظر فعله في معالج الانهيار
كلها ألغام باحتمال عالٍ جداً.
- حل logger من حاوية DI
- استخدام async / await
- رمي Task
- انتظار قفل
- تجميع JSON معقّد
- لمس كائن COM
- إظهار مربع واجهة
- ضغط
- إرسال HTTP / SMTP / Slack / Teams
- تحليل dump وتلخيصه
- ابتلاع الاستثناء والمواصلة
معالج الانهيار ليس تتمة لتدفق معالجة عادي. نميل إلى «كتابة محلية حد أدنى ثم الانتهاء».
5.3 ما يُفعل في معالج الانهيار
بالعكس، ما يُفعل بسيط جداً.
- منع الدخول المتعدد
- كتابة سطر واحد
- flush
- الإنهاء
بهذا الترتيب.
إن أمكن نستخدم:
- مجلداً مخصصاً أُنشئ مسبقاً
- مساراً تُأكد وجوده مسبقاً
- وجهة حفظ تُأكد ACL لها
.
في السجل العادي الإفراط في flush يثقّل، لكن علامة fatal عددها ضئيل جداً فيجوز هنا flush أقوى.
في .NET FileStream.Flush(true)، وفي native FlushFileBuffers، نميل إلى معاملة «هذا السطر وحده يُسقط إلى الأرض فوراً».
flowchart TB
accTitle: أربع خطوات لمعالج الانهيار
accDescr: مخطّط يبيّن أن ما يُفعل في معالج الانهيار أربعة فقط: منع الدخول المتعدد، وكتابة سطر واحد، وflush، والإنهاء، وأن علامة fatal عددها ضئيل جداً فيجوز flush أقوى حتى القرص.
j1["1. منع الدخول المتعدد"] --> j2["2. كتابة سطر واحد"]
j2 --> j3["3. flush"]
j3 --> j4["4. الإنهاء"]
j3 -.-> j5["هذا السطر وحده يُسقط إلى الأرض فوراً"]
الشكل 11: ما يُفعل في المعالج أربع خطوات فقط، ولا يُكسر هذا الترتيب.
5.4 لا تحاول المواصلة
تجاه استثناء unexpected ناجم عن خطأ برمجي، أأمن أن نرى آخر معالج جهاز تسجيل لا جهاز استعادة.
خصوصاً ما نريد جعل «لا نواصل» أساساً فيه هذه الناحية.
- حتى
NullReferenceExceptionأوInvalidOperationExceptionكان في وسط تحديث حالة مشتركة - استثناء unexpected على خيط الواجهة
- استثناء unexpected تسرّب من حلقة مراقبة أو حلقة أب
AccessViolationExceptionStackOverflowException- شذوذ على حدود native
- invalid parameter / purecall / terminate في CRT
رغبة «لا نريد إسقاطه» مفهومة، لكن البقاء حيّاً بنصف كسر أشق غالباً في التشخيص والتشغيل.
عند الإنهاء أأمن تصميماً النظر في API إنهاء فوري مثل Environment.FailFast في .NET أو RaiseFailFastException و __fastfail في native، دون توقع finally أو تنظيف عادي.
flowchart TB
accTitle: جهاز تسجيل لا جهاز استعادة
accDescr: مخطّط يبيّن أنه تجاه استثناء غير متوقع ناجم عن خطأ برمجي أأمن رؤية آخر معالج كجهاز تسجيل لا استعادة، وأن التسجيل ثم الإنهاء بـ API إنهاء فوري أأمن من البقاء حيّاً بنصف كسر.
k1["استثناء ناجم عن خطأ برمجي"] --> k2["البقاء حيّاً بنصف كسر"]
k1 --> k3["سجّل ثم أنهِ"]
k2 -.-> k4["التشخيص والتشغيل أشق"]
k3 --> k5["النظر في API إنهاء فوري مثل FailFast"]
الشكل 12: آخر معالج ليس جهاز استعادة بل جهاز تسجيل ثم إنهاء.
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");
// 通常ログ・ダンプ・watchdog 記録と突き合わせるための鍵
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// 多重突入を防ぐフラグ。0 なら未書き込み
private static int _written;
/// <summary>アプリ起動直後に 1 回だけ呼ぶ。</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);
// لا تستدعِ مسلسلاً؛ ابنِ سطراً واحداً بوصل السلاسل فقط
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 يسقط إلى القرص لا إلى ذاكرة التخزين المؤقت لنظام التشغيل
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");
}
}
ما تحفظه هذه الستون سطراً الأربعة المذكورة في 5.3 فقط.
- منع الدخول المتعدد بـ
Interlocked.Exchange - كتابة سطر واحد بوصل سلاسل فقط
- الإسقاط حتى القرص بـ
Flush(true) - من
UnhandledExceptionلا نواصل
Environment.FailFast يكتب الرسالة في سجل أحداث تطبيقات Windows ثم ينهي العملية فوراً، ويضم ذلك المضمون أيضاً في تقرير الخطأ. أي أن FailFast نفسه يزيد دليلاً واحداً. لكن كما سبق، استدعاؤه في مسار الاستثناء غير المعالَج يغيّر مظهر الـ dump، فاختر موضع الاستدعاء.
flowchart TB
accTitle: اختر موضع استدعاء FailFast
accDescr: مخطّط يبيّن أن Environment.FailFast يكتب في سجل أحداث التطبيقات ثم ينهي فوراً فيزيد دليلاً واحداً، لكن استدعاءه في مسار الاستثناء غير المعالَج يبدّل سبب الـ dump الباقي في WER من الاستثناء الأصلي إلى FailFast، لذا يُختار موضع الاستدعاء.
l1["Environment.FailFast"] --> l2["يكتب في سجل الأحداث وينهي فوراً"]
l2 --> l3["يزيد دليلاً واحداً"]
l1 -.-> l4["إن استُدعي في مسار الاستثناء غير المعالَج"]
l4 -.-> l5["سبب الـ dump يُبدَّل إلى FailFast"]
الشكل 13: FailFast يزيد دليلاً، لكن استدعاؤه داخل استثناء غير معالَج يبدّل السبب.
6. نقاط انتباه حسب الإطار
6.1 مشترك في .NET: AppDomain.CurrentDomain.UnhandledException
هذا مفيد كـ آخر إشعار. لكن نتجنّب استعادة ثقيلة هنا.
أساس الاستخدام بسيط.
- كتابة علامة الانهيار النهائية
- إن لزم إبقاء رسالة حد أدنى في Windows Event Log
- لا نواصل
- لا ننتظر ولا نعيد المحاولة هنا
UnhandledException مريح، لكن أأمن ألا نفترض أن التطبيق يُعاد هنا إلى حالة صحية.
6.2 WinForms: Application.ThreadException
الصعب هنا أنه يلتقط استثناء خيط الواجهة غير المعالَج فيستطيع المواصلة ظاهرياً.
لغرض تحويل أخطاء متوقعة في إدخال أعمال إلى مربع ربما، لكن لا يناسب غرض المواصلة بعد استثناء unexpected ناجم عن خطأ برمجي.
إن كانت أولوية التحقيق في السبب حقاً، فأأمن:
- تسجيل حد أدنى فقط في
ThreadException - أو الميل إلى
UnhandledExceptionMode.ThrowException - ثم إنهاء العملية وإبقاء dump وسجلات
.
6.3 WPF: Application.DispatcherUnhandledException
في WPF مشابه.
- الاستثناء على خيط الواجهة هو الهدف الرئيس فقط
- جعل
Handled = trueيتيح المواصلة ظاهرياً - لكن فعل ذلك تجاه خطأ برمجي يسهل إزاحة حالة الشاشة عن الحالة الداخلية
لذا حتى في WPF أسلم عدم استخدامه كجهاز إطالة للمواصلة، واستخدامه كمدخل تسجيل.
flowchart TB
accTitle: لا نطيل بأحداث الواجهة
accDescr: مخطّط يبيّن أن ThreadException في WinForms و DispatcherUnhandledException في WPF يستطيعان المواصلة ظاهرياً، لكن تجاه خطأ برمجي تنزاح حالة الشاشة عن الداخلية، لذا يُستخدمان كمدخل تسجيل ثم يُنهى مع إبقاء dump وسجلات.
m1["استثناء غير معالَج على خيط الواجهة"] --> m2["يمكن المواصلة ظاهرياً"]
m2 -.-> m3["تنزاح الشاشة عن الحالة الداخلية"]
m2 --> m4["يُستخدم كمدخل تسجيل"]
m4 --> m5["يُنهى مع إبقاء dump وسجلات"]
الشكل 14: أحداث واجهة WinForms / WPF مدخل تسجيل لا جهاز إطالة.
6.4 لا تجعل TaskScheduler.UnobservedTaskException المسار الرئيس
هذا ليس «آخر حصن قبل السقوط».
يمكن استخدامه كمساعد لكشف فوات استثناء Task،
لكنه ضعيف كمسار تسجيل يقيني عند الانهيار.
لذا يجوز استخدامه لغرض:
- إيجاد فوات رصد الاستثناء مبكراً
- إبراز ثغرات تصميم
Taskأثناء التطوير
لكن الأفضل ألا يكون بطل آخر معالج انهيار.
6.5 native Win32 / C++: لا تبالغ في الثقة بـ SetUnhandledExceptionFilter
في الجهة الأصلية يسهل توقع SetUnhandledExceptionFilter.
لكن هذا يعمل في سياق faulting thread، فيتأثر بـ:
- مكدس غير صالح
- تكرار عميق
- كومة مكسورة سلفاً
- إمساك قفل عند وقوع الاستثناء
لذا يناسب اعتبار SetUnhandledExceptionFilter
مدخل best effort لاستلام آخر إشعار.
6.6 native C++ يلتقط أيضاً مسار إنهاء CRT
في native C++، النظر إلى SEH غير المعالَج وحده يفوّت.
تحديداً ننظر إلى هذه الناحية.
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
هذه العائلة لالتقاط «مسار إنهاء» نابع من وقت تشغيل C أو C++.
في العمل أسلم:
- كتابة علامة الانهيار النهائية أيضاً في هذه المعالجات
- لكن بلا استعادة ثقيلة
- الإنهاء بيقين
- الأدلة الرئيسة تُترك لـ WER / dump
flowchart TB
accTitle: SEH وحده يفوّت
accDescr: مخطّط يبيّن أنه في native C++ النظر إلى SetUnhandledExceptionFilter وحده يفوّت مسار إنهاء نابع من CRT أو وقت تشغيل C++، لذا تُبقى أدنى تسجيل أيضاً في معالجات invalid parameter و purecall و terminate، وتُترك الأدلة الرئيسة لـ WER و dump.
n1["SetUnhandledExceptionFilter وحده"] --> n2["يفوّت مسار إنهاء CRT"]
n2 --> n3["التقاط invalid parameter / purecall / terminate أيضاً"]
n3 --> n4["أدنى تسجيل ثم إنهاء بيقين"]
n4 -.-> n5["الأدلة الرئيسة تُترك لـ WER / dump"]
الشكل 15: في native C++ لا ينسد الفوات إلا بالتقاط مسار إنهاء جهة CRT إضافة إلى SEH.
7. اجعل WER LocalDumps الأساس
هنا قوي جداً في العمل.
7.1 الموصى به أولاً WER LocalDumps
بمعنى «إبقاء أدلة حد أدنى أقرب إلى اليقين بعد السقوط»، WER LocalDumps الأسهل تعاملاً أولاً.
السبب بسيط.
- يمكن إبقاء dump من جهة نظام التشغيل
- يسهل إدخاله بلا أدوات إضافية
- يمكن ضبطه لكل تطبيق
- يمكن تهريب الأدلة الرئيسة عند الانهيار إلى خارج العملية
ما لا يُفهم من السجلات وحدها يمكن رؤيته لاحقاً:
- أي خيط سقط
- على أي مكدس سقط
- أي حدود وحدة كانت
- أين الشبهة: managed / native / COM / SDK
وهذا قوي.
flowchart TB
accTitle: لماذا WER LocalDumps قوي
accDescr: مخطّط يبيّن أن WER LocalDumps يبقي dump من جهة نظام التشغيل ويمكن ضبطه لكل تطبيق ويهرب الأدلة الرئيسة إلى خارج العملية، ويمكن لاحقاً رؤية أي خيط سقط على أي مكدس وأي حدود وحدة.
p1["WER LocalDumps"] --> p2["إبقاء dump من جهة نظام التشغيل"]
p1 --> p3["يمكن ضبطه لكل تطبيق"]
p2 --> p4["يمكن تهريب الأدلة الرئيسة إلى خارج العملية"]
p3 --> p4
p4 -.-> p5["تُرى الخيوط والمكدس وحدود الوحدات"]
الشكل 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
- لا نعرف بناء أي التزام
كحد أدنى نُبقي هذا فقط.
- الثنائيات الموزَّعة
- PDB المقابل
- الإصدار
- تاريخ البناء ووقته
- معرّف الالتزام
- إصدار المثبّت
جمع الـ dump وحفظ PDB مجموعة.
flowchart TB
accTitle: جمع الـ dump وحفظ PDB مجموعة
accDescr: مخطّط يبيّن أنه حتى إن أُخذ dump يتعذر القراءة لاحقاً إن لم يبق EXE أو DLL ذلك الوقت وPDB وبناء أي التزام، لذا يُحفَظ الثنائي الموزَّع وPDB والإصدار ومعرّف الالتزام مع جمع الـ dump.
r1["إبقاء الـ dump وحده"] --> r2["لا PDB ولا ثنائي ذلك الوقت"]
r2 --> r3["يتعذر القراءة لاحقاً"]
r3 -.->|"لذا"| r4["حفظ الثنائي الموزَّع وPDB"]
r4 --> r5["إبقاء الإصدار ومعرّف الالتزام أيضاً"]
الشكل 18: الـ dump لا يُقرأ إلا إذا بقي PDB والثنائي للبناء نفسه.
7.6 حتى فتح الـ dump المأخوذ أولاً
«الـ dump متراكم لكن لم يفتحه أحد» شائع حقاً. نترك التعمّق لمقالة متخصصة، ونضع هنا عشر دقائق أولى فقط.
التحضير اثنان.
- جمع PDB والثنائيات الموزَّعة للبناء نفسه لذلك 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 |
الانتقال إلى سياق السجلات عند وقوع الاستثناء. إن نُسي هذا نرى مكدس انتظار جهة WER لا موضع السقوط |
!analyze -v |
تحليل الاستثناء الحالي تلقائياً وعرض تفصيلي. نقرأ هنا أولاً |
~*k |
إخراج مكدس الاستدعاء لكل الخيوط. يُرى ماذا كان يفعل غير خيط السقوط |
lm v |
إخراج الوحدات المحمَّلة وإصداراتها. يُستخدم لمطابقة الموزَّع برقم البناء |
إن وصلت إلى هنا و«لا يظهر اسم دالة» أو «لا يظهر رقم سطر»، فالسبب تقريباً PDB من بناء مختلف. ارجع إلى إدارة الإصدار في 7.5.
flowchart TB
accTitle: الدقائق العشر الأولى لفتح الـ dump
accDescr: مخطّط يبيّن التدفق الأول: جمع PDB والثنائيات الموزَّعة للبناء نفسه، وفتح الـ dump في WinDbg، والانتقال إلى سياق وقوع الاستثناء ثم قراءة التحليل، وإن لم يظهر اسم دالة فـ PDB بناء مختلف فنرجع إلى إدارة الإصدار.
s1["جمع PDB والثنائي للبناء نفسه"] --> s2["فتح الـ dump في WinDbg"]
s2 --> s3["الانتقال إلى سياق وقوع الاستثناء ثم التحليل"]
s3 -.-> s4["نسيان .ecxr يرى مكدس انتظار جهة WER"]
s3 --> s5{"هل يظهر اسم دالة أو رقم سطر؟"}
s5 -->|"لا"| s6["PDB بناء مختلف، ارجع إلى إدارة الإصدار"]
الشكل 19: تحليل الـ dump يبدأ بهذا الترتيب: تحضير، فتح، انتقال إلى سياق الاستثناء.
حديث جمع وتحليل أعمق ملخّص في المقالات ذات الصلة في النهاية.
8. طريقة التفكير عند استخدام MiniDumpWriteDump أو مقرّر انهيار خاص
توجد مشاهد يلزم فيها تنفيذ خاص.
- نريد زر «حفظ معلومات تشخيص» من الواجهة
- نريد أيضاً حزم سجلات وملفات إعداد
- نريد معالجة مجموعة عمليات ابن معاً
- نريد إدخال إخفاء خاص قبل الرفع التلقائي
لكن الأهم هنا ألا نحمّل جهة السقوط أكثر مما ينبغي حتى معالجة أخذ dump.
8.1 عملية أخرى أفضل من self-dump
MiniDumpWriteDump قوي، لكن
استدعاؤه من عملية أخرى أأمن من استدعائه من داخل العملية المنهارة نفسها.
نموذجياً يصير التكوين هكذا.
- جسم worker يكشف شذوذاً
- إن أمكن يُشعر helper بحدث أو named pipe
- helper يأخذ dump لـ worker
- helper يحزم سجلات
tailوملفات إعداد - helper يضعها في طابور رفع بعد الإنهاء
عندها حتى لو انكسر worker تبقى جهة helper سليمة.
sequenceDiagram
accTitle: أخذ dump عبر عملية helper أخرى
accDescr: مخطّط يبيّن تدفقاً يكشف فيه جسم worker شذوذاً فيُشعر helper بحدث أو named pipe، ويأخذ helper السليم dump لـ worker ويحزم سجلات وملفات إعداد ويضعها في طابور رفع بعد الإنهاء.
participant W as جسم worker
participant H as helper
W->>H: إشعار بالشذوذ بحدث أو أنبوب
H->>W: أخذ dump لـ worker
H->>H: حزم سجلات tail وملفات إعداد
H->>H: وضعها في طابور رفع بعد الإنهاء
Note over H: حتى لو انكسر worker يبقى helper سليماً
الشكل 20: أخذ dump يُترك لعملية helper سليمة لا لجهة السقوط.
8.2 إن لزم حتماً داخل العملية فأملِ إلى خيط مخصص
حتى إن تعذر جعله عملية أخرى، جعل خيط مخصص للـ dump وحده أفضل.
لكن حتى عندها الجوهر best effort. لا يصير «أدخلنا تنفيذ dump خاصاً إذن 100% اطمئنان».
8.3 الثقيل يُدار إلى ما بعد التشغيل التالي
ما يسهل الرغبة في فعله في مقرّر خاص:
- ضغط zip
- مطابقة مع معلومات رموز
- رفع إلى خادم
- التقاط شاشة
- جلب معلومات إضافية من DB
هذه تُدار إلى بعد إعادة التشغيل أو جهة helper لا عند الانهيار.
9. ماذا يتغيّر بإدخال عملية مراقبة
في أنظمة التشغيل الطويل تؤثّر عملية المراقبة كثيراً.
9.1 ما تبقيه عملية المراقبة
في جهة watchdog / launcher / الخدمة الأب يمكن إبقاء معلومات كهذه.
- وقت بدء العملية الابن
- وسائط التشغيل
- PID
- إصدار الهدف المراقب
- وقت آخر استلام heartbeat
- وقت الإنهاء
- exit code
- عدد restart
- وجود dump من عدمه
- هل أُعيد التشغيل
بوجود هذا وحده تُرى كثيراً:
- هل انهار حقاً
- هل كان إغلاق نظام التشغيل
- هل أغلقه المستخدم
- هل قُتل من hang
- كم مرة دارت حلقة إعادة التشغيل
flowchart TB
accTitle: ما يمكن تمييزه بسجل watchdog
accDescr: مخطّط يبيّن أنه إذا أبقت عملية المراقبة exit code ووقت الإنهاء وآخر استلام heartbeat وعدد restart يمكن التمييز من الخارج بين انهيار حقيقي وإغلاق نظام التشغيل وإنهاء مستخدم وقتل من hang وحلقة إعادة تشغيل.
t1["exit code ووقت الإنهاء"] --> t4["يمكن التمييز من الخارج"]
t2["آخر استلام heartbeat"] --> t4
t3["عدد restart"] --> t4
t4 -.-> t5["انهيار أم إغلاق نظام أم إنهاء مستخدم"]
الشكل 21: بوجود سجل watchdog يمكن تمييز نوع الإنهاء من الخارج.
9.2 حالات تناسبه خصوصاً
يجوز النظر بإيجابية في الفصل في حالات كهذه.
- worker يحمل SDK مورّد
- معالجة صور / فيديو / device I/O
- حلقة أب للمراقبة أو الاستطلاع
- تنفيذ سكربت أو إضافة
- مضيف أصل COM / ActiveX قائم
- جسر 64bit / 32bit أو تشغيل متبادل
حبس المعالجة الخطرة في worker واحد يسهّل تصميم السجلات وتصميم الاستعادة.
10. أخطاء شائعة
هنا نجمع مزالق على مستوى التصميم. حديث مستوى الإجراءات «ماذا يُحظر في معالج الانهيار» ملخّص في 5.2، فمن كان في التنفيذ فلينظر هناك. البنود التي تبدو متداخلة (مثل إرسال HTTP في 10.3) يكتب 5.2 «لماذا سيئ داخل المعالج»، وهنا «ماذا يحدث في التشغيل نتيجة لذلك».
10.1 المواصلة بعد إخراج سجل فقط بـ catch (Exception)
الأكثر شيوعاً والأخطر.
- تبقى تغييرات وسط الطريق
- تنكسر الحالة المشتركة
- تزيد الأعطال اللاحقة
- تضيع نقطة السبب الحقيقية
كثيراً ما يطول الحادث مقابل زيادة سجل واحد.
10.2 الإيمان بطابور async logger وحده
السجل غير التزامني نفسه ليس سيئاً. المشكلة التراكم في الطابور نفسه حتى في fatal path ثم الانتهاء.
إن توقف العامل في لحظة السقوط يختفي ذلك الطابور معه.
أأمن امتلاك مهرب الكتابة المباشرة في fatal path فقط.
10.3 إرسال HTTP في معالج الانهيار
يرغب في التنفيذ، لكنه خطر جداً.
- DNS
- TLS
- proxy
- مصادقة
- مهلة
- انتظار إعادة إرسال
كلها تركب سياق السقوط.
الإرسال يكون بعد إعادة التشغيل.
10.4 يوجد dump لكنه لا يرتبط بالسجل العادي
هذا كثير.
- لا session في اسم ملف الـ dump
- لا PID / session في جهة السجل
- لا PID في جهة watchdog
- رقم البناء غير متطابق
نتيجة لذلك تبدو الأدلة الثلاثة أحاديث منفصلة.
flowchart TB
accTitle: فشل عدم ارتباط الأدلة
accDescr: مخطّط يبيّن أنه إن لم يكن session في اسم الـ dump، أو لم يكن PID أو session في جهة السجل، أو لم يتطابق رقم البناء، تبدو أدلة الـ dump والسجل وسجل watchdog ثلاثة حوادث منفصلة.
u1["لا session في اسم الـ dump"] --> u4["تبدو الأدلة الثلاثة أحاديث منفصلة"]
u2["لا PID / session في السجل"] --> u4
u3["رقم البناء غير متطابق"] --> u4
الشكل 22: نقص المعرّفات المفتاحية يقطع ارتباط الأدلة رغم وجودها.
10.5 الإطالة بأحداث الاستثناء غير المعالَج في WinForms / WPF
ظاهرياً «يتوقف السقوط» فيُفرَح به أولاً. لكن في الواقع يسهل صنع حالة زومبي:
- الشاشة وحدها باقية
- العامل ميت
- الزر نشط وحده باقٍ
- لا نعرف إن نجح الحفظ
.
10.6 عدم النظر إلى مسار الإنهاء في الجهة الأصلية
الاطمئنان بـ SetUnhandledExceptionFilter وحده يفوّت جهة:
- invalid parameter
- purecall
- terminate
- fast fail
.
في native C++ الأفضل الوعي بـ مسار إنهاء جهة CRT / وقت تشغيل C++ أيضاً لا SEH وحدها.
11. قائمة تحقق حد أدنى للإدخال
إن استُوفي التالي فهو موجّه جداً إلى الميدان.
- السجل العادي يبقى بسطر واحد لكل حدث
- كل السجلات فيها UTC و PID و TID و version و session
- يبقى
ProcessStartوProcessExit - أحداث الحدود المهمة تُفرَّغ تزامنياً
- يوجد ملف مخصص لعلامة الانهيار النهائية
- fatal path لا يمر عبر async logger
- WER LocalDumps مضبوط لكل تطبيق
- ACL وجهة حفظ الـ dump مُتحقَّق منها
- PDB والثنائيات الموزَّعة محفوظة
- يمكن كشف إنهاء غير طبيعي سابق عند التشغيل التالي
- الضغط / الرفع / الإشعار يُنفَّذ بعد إعادة التشغيل أو في عملية أخرى
- في native C++ رُتّب أيضاً invalid parameter / purecall / terminate
- أُسقط عمداً على جهاز تحقق وتُأكد أنه يبقى حقاً
آخر سطر مهم خصوصاً. التصميم وحده بلا معنى، ويلزم حتماً «اختبار الأخذ حتى النهاية».
12. إلى أي حد نختبر
جمعنا بنوداً نريد تأكيدها في جدول.
| الاختبار | ماذا نؤكّد |
|---|---|
| استثناء managed غير معالَج | هل تكتمل السجل العادي وعلامة fatal والـ dump كلها |
| استثناء خيط الواجهة | هل مسار أحداث WinForms / WPF كما هو مفترض |
| استثناء خيط عامل | هل يصل إلى AppDomain.UnhandledException، وهل يستطيع watchdog الكشف |
| استثناء native | هل يُؤخذ dump من WER حقاً |
| invalid parameter / terminate | هل يبقى أدنى تسجيل حتى في مسار CRT / وقت تشغيل C++ |
| قتل قسري | حتى إن تعذر داخل العملية، هل تستطيع جهة watchdog تسجيل unexpected exit |
| إعادة تشغيل | هل يعمل الإشعار والاسترداد والرفع بعد التشغيل التالي |
المهم تأكيد «بهذا الشرط يبقى هذا الملف» لا «يفترض أن يخرج سجل إن طار استثناء».
13. الخلاصة
إن أردنا إبقاء معلومات لازمة للتحقيق حتى لو سقط تطبيق Windows باستثناء ناجم عن خطأ برمجي، فمحور التفكير بسيط جداً.
- لا نعتمد على العملية المنهارة وحدها
- نفصل إلى سجل عادي، وعلامة انهيار نهائية، وأدلة جهة نظام التشغيل / عملية أخرى
- عند الانهيار نُبقي محلياً وبإيجاز فقط
- المعالجة الثقيلة تُدار إلى بعد إعادة التشغيل أو عملية أخرى
- نجعل WER LocalDumps الأساس
- نجعل الأساس التسجيل ثم الإنهاء لا المواصلة
في النهاية، أقوى من «الاجتهاد في آخر سطر» «صنع تكوين يمكن تتبّعه حتى بلا آخر سطر».
flowchart TB
accTitle: تكوين لا يعتمد على آخر سطر
accDescr: مخطّط يبيّن أن صنع تكوين يمكن تتبّعه حتى بلا آخر سطر أقوى من الاجتهاد في آخر سطر، ومع ذلك تُبقى علامة الانهيار النهائية قصيرة في ملف منفصل، وتُحمل الأدلة الرئيسة على dump من WER والسجل العادي حتى اللحظة السابقة.
v0["الشكل المستهدف"] --> v2["تكوين يمكن تتبّعه حتى بلا آخر سطر"]
v2 --> v3["العلامة تُبقى قصيرة في ملف منفصل"]
v2 --> v4["الأدلة الرئيسة dump وسجل عادي"]
الشكل 23: أقوى من الاجتهاد في آخر سطر صنع تكوين يمكن تتبّعه بدونه.
ومع ذلك نريد آخر سطر، لذا علامة الانهيار النهائية تُبقى قصيرة في ملف منفصل. والأدلة الرئيسة حقاً تُحمَل على dump من WER والسجل العادي حتى اللحظة السابقة. هذه في عمل تطبيقات Windows طريقة ثابتة جداً.
مقالات ذات صلة
- مدخل إلى جمع crash dump في Windows - WER/ProcDump/WinDbg
- جدول قرار: هل ننهي أم نواصل عند استثناء غير متوقع
روابط مرجعية
- Microsoft Learn, Collecting User-Mode Dumps
- Microsoft Learn, Using WER
- Microsoft Learn, MiniDumpWriteDump function
- Microsoft Learn, SetUnhandledExceptionFilter function
- Microsoft Learn, System.AppDomain.UnhandledException event
- Microsoft Learn, Application.ThreadException Event
- Microsoft Learn, Application.DispatcherUnhandledException Event
- Microsoft Learn, TaskScheduler.UnobservedTaskException Event
- Microsoft Learn, Environment.FailFast
- Microsoft Learn, Registering for Application Recovery
- Microsoft Learn, RegisterApplicationRecoveryCallback
- Microsoft Learn, WerRegisterFile
- Microsoft Learn, _set_invalid_parameter_handler
- Microsoft Learn, _set_purecall_handler
- Microsoft Learn, set_terminate
- Microsoft Learn, __fastfail
- Microsoft Learn, FileStream.Flush(Boolean)
- Microsoft Learn, !analyze (WinDbg)
- Microsoft Learn, .ecxr (Display Exception Context Record)
- Microsoft Learn, Symbol path for Windows debuggers
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
مدخل إلى جمع dump انهيار تطبيقات Windows: WER / ProcDump / WinDbg
لتعقّب انهيار تطبيق Windows صعب الإعادة، ترتّب المقالة الفصل بين WER LocalDumps وProcDump وMiniDumpWriteDump وWinDbg، وما ينبغي الانتباه ...
جدول قرار: إنهاء أم استمرار بعد استثناء غير متوقّع
عند وقوع استثناء غير متوقّع، هل تُنهي التطبيق أم تستمر؟ المقالة ترتّب الحكم من تلف الحالة والآثار الجانبيّة الخارجيّة والخيوط والحدود الأ...
ما الذي يفعله بدء التشغيل السريع فعليّاً ── لماذا «إيقاف التشغيل» في Windows ليس إعادة التشغيل
إيقاف التشغيل في Windows إغلاق هجين افتراضيّاً، فيُحفَظ النواة وبرامج التشغيل في hiberfil.sys. لماذا لا تُعاد تهيئتها إلا بإعادة التشغيل،...
Time Travel Debugging ── تسجيل الأخطاء التي لا تتكرّر في التطبيقات طويلة التشغيل وإرجاعها
خطأ يظهر مرّة في الشهر لا يترك في تفريغ الانهيار سوى نتيجته. سجّل التنفيذ وأرجعه بـ WinDbg Time Travel Debugging (TTD): TTD.exe والمخزن ا...
لماذا تتعطّل الوسائط ── قواعد وسائط سطر أوامر Windows
في Windows لا توجد مصفوفة وسائط؛ يصل إلى CreateProcess سلسلة واحدة ويتولّى الطرف المستقبل تقسيمها. نشرح قواعد CommandLineToArgvW وCRT و.N...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
التحقيق في الأخطاء وتحليل السبب الجذري
الأعطال التي لا تظهر إلّا في بيئة العميل، والإنهاءات غير الطبيعيّة نادرة التكرار، وتحليل الأسباب عبر مطابقة ملفّات التفريغ بالسجلّات، مواضيع تتوافق جيّداً مع التحقيق في الأخطاء وتحليل الأسباب.
تطوير تطبيقات ويندوز
كيفيّة تصميم السجلّات المعتادة وWER وwatchdog في WPF وWinForms والتطبيقات المقيمة وخدمات Windows تتّصل مباشرة بتطوير تطبيقات Windows نفسه.
الأسئلة الشائعة
أسئلة شائعة حول موضوع هذه المقالة.
- هل يستطيع التطبيق المنهار نفسه إبقاء سجل بيقين؟
- لا. إذا أدخلنا فساد المكدس وفساد الذاكرة وfast fail والإنهاء القسري وانقطاع الكهرباء، فآخر سجل داخل العملية best effort في جوهره. في العمل نقسم إلى ثلاث طبقات: سجل زمني في الأوقات العادية، وعلامة انهيار نهائية في لحظة السقوط، وأدلة انهيار يبقيها نظام التشغيل أو عملية أخرى، فلا نعتمد على داخل العملية المنهارة وحدها. الأكثر أماناً الجمع بين سجل عادي + علامة انهيار نهائية + WER LocalDumps.
- ماذا يُحظر فعله داخل معالج الانهيار؟
- المعالجة الثقيلة عامة. حل logger من حاوية DI، وasync/await، وانتظار قفل، وتوليد JSON معقّد، وتشغيل كائنات COM، ومربعات واجهة، وضغط، وإرسال 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 والمواصلة؟
- خطر تجاه استثناء unexpected ناجم عن خطأ برمجي. يمكن المواصلة ظاهرياً بـ Handled=true، لكن يسهل صنع حالة زومبي: الشاشة باقية والعامل ميت، أو الزر نشط ولا نعرف إن نجح الحفظ. أحداث الاستثناء غير المعالَج ليست جهاز استعادة بل مدخل تسجيل. بعد كتابة علامة الانهيار النهائية ننهي دون مواصلة، ونحمل الأدلة الرئيسة على dump من WER والسجل العادي حتى اللحظة السابقة. للإنهاء ننظر في API إنهاء فوري مثل Environment.FailFast.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.