كيف نُبقي سجلّات الأعطال في تطبيقات Windows حتى عند الموت بسبب أخطاء برمجية - أفضل الممارسات مع WER وعلامات نهائية وتصميم watchdog
· 小村 豪 · تطوير Windows, معالجة الاستثناءات, تسجيل, WER, crash dumps, تحقيق العلل
أكثر الحالات إيلاماً في استكشاف أخطاء تطبيقات Windows هي هذه:
تعلم أنّ العملية تعطّلت، لكنّك لا تعرف السبب، والأدلّة شحيحة بدرجة لا تكفي لإعادة بنائها لاحقاً.
تصبح هذه الحالة مكلفة جدّاً خصوصاً في الأمثلة التالية:
- لا يحدث الـ crash إلّا في بيئات العملاء
- لا يحدث إلّا بعد تشغيل طويل
- تطبيق WPF أو تطبيق WinForms أو خدمة Windows أو تطبيق مقيم منخفض التكرار
- يدخل فيه COM أو P/Invoke أو native DLLs أو SDKs من جهة موردين
- لديك «نصّ الاستثناء» لكن ليس لديك السياق السابق مباشرةً للـ crash
من المهمّ أن نقول الحقيقة منذ البداية:
لا يمكنك ضمان أنّ العملية المتعطّلة نفسها ستكتب دائماً السجلّ النهائي بنجاح.
حالما تُدخل فساد الـ stack وفساد الـ heap ومسارات fast-fail والإنهاء القسري وانقطاع الكهرباء، يصبح السجلّ النهائي داخل العملية بطبيعته آليّة best effort.
ما يعمل أفضل بكثير في الواقع هو تصميم لا يعتمد على العملية المتعطّلة وحدها.
بمعنى آخر، فكّر في ثلاث طبقات:
- سجلّات زمنية اعتيادية أثناء التشغيل العادي
- علامة crash نهائية مُصغّرة في لحظة الفشل
- أدلّة crash يلتقطها نظام التشغيل أو عملية أخرى
هذه المقالة تُنظّم هذا النهج لتطبيقات سطح مكتب Windows، والأدوات المقيمة، وخدمات Windows، وأدوات تكامل الأجهزة، بهدف عدم فقدان قابلية التشخيص حتى حين يموت البرنامج بسبب خطأ برمجي حقيقي.
1. الإجابة المختصرة
إليك الخلاصات أوّلاً.
- القاعدة الأهمّ على الإطلاق: لا تُراهن بكلّ شيء على معالج «آخر سجلّ» داخل العملية واحد.
- الأساس الأكثر أماناً عمليّاً غالباً هو: سجلّات عادية + علامة crash نهائية + WER LocalDumps.
- إذا كان التطبيق يعمل لفترة طويلة، أو يتحكّم بأجهزة، أو يُحمّل plugins، أو يخلط native SDKs، فإضافة watchdog / launcher / service يُقوّي التصميم بشكل ملحوظ.
- في معالجات الـ crash، القاعدة هي: لا تقم بعمل ثقيل. لا ضغط، لا رفع HTTP، لا حلّ DI، لا UI dialogs، لا توليد JSON معقّد.
- في وقت الـ crash، اترك سجلّاً محلّيّاً قصيراً فقط. ادفع الضغط والرفع والإشعار إلى التشغيل التالي أو عملية سليمة أخرى.
- استخدام
ThreadExceptionفي WinForms أوDispatcherUnhandledExceptionفي WPF لإبقاء التطبيق يتعرّج إلى الأمام بعد خطأ برمجي خطر عادةً. - في كلّ من .NET والكود الـ native، يكون التصميم الأكثر أماناً عند الفشل المشتبه به بفساد هو سجّل ثمّ أنهِ بدلاً من استعد ثمّ تابع.
- إذا جمعت dumps، فعليك أيضاً حفظ PDBs وملفّات الـ binary المنشورة المطابقة، وإلّا يصبح الـ dump أقلّ نفعاً بكثير لاحقاً.
أفضل ممارسة عملية هي:
لا تحاول فعل كلّ شيء في لحظة الفشل. وزّع المهمّة عبر مراحل ما قبل الـ crash، ووقت الـ crash، وما بعد إعادة التشغيل.
2. لماذا لا يمكن «ضمان» المنطق داخل العملية وحده
إذا بقيت هذه النقطة ضبابيّة، تبقى المعمارية ضبابيّة.
2.1 سياق الـ thread المتعطّل نفسه قد يكون متضرّراً بالفعل
قد تعمل خطّافات الاستثناءات غير المعالجة ومرشّحات الاستثناء العلوية في سياق الـ thread الفاشل نفسه.
في تلك اللحظة، تكون مشكلات كهذه طبيعيّة جدّاً:
- الـ stack غير آمن أصلاً
- فساد الـ heap يجعل التخصيص الإضافي غير آمن
- الانتظار قد يُسبّب deadlock لأنّ الفشل وقع وtest lock محتجز
- الـ logger يعتمد على كائنات قد تكون متضرّرة بالفعل
لذا ينبغي معاملة الـ last-chance handler ليس كمكان «ما زال أيّ شيء ممكناً فيه» بل كمكان يكون فيه القليل جدّاً آمناً فعلاً.
2.2 مسارات fast-fail والفشل بأسلوب الفساد مُصمّمة حول الحدّ الأدنى من العمل داخل العملية
عند وجود فساد ذاكرة أو حالة قاتلة مماثلة، كثيراً ما يكون الأكثر أماناً ألّا نتوقّع سلوك معالجة الاستثناء العاديّ.
خصوصاً على الجانب الـ native، تُصمّم خروجات __fastfail والحالات المشتبه بفسادها عمداً حول الفكرة:
أنهِ فوراً مع أقلّ عمل إضافي ممكن
وهذا يُؤدّي طبيعيّاً إلى هذا النموذج الذهني:
- إن كُتب آخر سطر سجلّ داخل العملية، فهذا حظّ
- ينبغي أن يأتي دليل الـ crash الرئيسي من نظام التشغيل أو من عملية أخرى
2.3 أحداث الاستثناءات غير المعالجة في .NET ليست أيضاً مكاناً لمنطق استعادة ثقيل
AppDomain.UnhandledException في .NET مفيد، لكن من الأكثر أماناً معاملته كمكان لـ تسجيل نهائي قصير فقط.
مثلاً:
- قد يبقى متأثّراً بالأقفال المحتجزة عند نقطة الـ crash
- ليس مكاناً آمناً عالميّاً لكلّ فشل بأسلوب الفساد
- فرض سياسة استمرارية هناك يميل إلى إبقاء البرنامج حيّاً في حالة نصف معطوبة
لذا:
«حدث الاستثناء غير المعالج» يعني «نقطة إخطار نهائية»، لا «نقطة استعادة آمنة».
3. المعمارية الموصى بها - فصل عمل وقت الـ crash عن عمل ما بعد إعادة التشغيل
النموذج الأنظف هو الفصل بين:
- ما تحاول فعله بينما العملية تفشل
- ما تفعله فقط بعد إعادة التشغيل أو من عملية سليمة أخرى
| المرحلة | الهدف | أين تعمل | ماذا ينبغي أن تفعل |
|---|---|---|---|
| التشغيل العادي | حفظ السياق الزمني | داخل التطبيق | structured logs، heartbeat، أحداث حدوديّة |
| وقت الـ crash | ترك حدّ أدنى من الدليل | التطبيق + نظام التشغيل | علامة crash نهائية، WER dump |
| فور الخروج | اكتشاف الإنهاء غير المتوقّع | عملية أخرى | تسجيل exit code، اتّخاذ قرار إعادة التشغيل، إخطار عند الحاجة |
| بعد إعادة التشغيل | إجراء متابعة أثقل | عملية جديدة سليمة | ضغط، رفع، إخطار المستخدم، تدوير السجلّات القديمة |
حالما تُقسّم العمل بهذه الطريقة، يصبح التصميم أكثر استقراراً بكثير.
3.1 الأساس الأدنى
لأداة عمل أصغر أو تطبيق WPF / WinForms داخلي، غالباً ما يكفي ما يلي:
- سجلّات عادية في ملفّ append-only محلّي
- ملفّ علامة crash نهائية مُخصّص واحد
- WER LocalDumps
- في التشغيل التالي، «انتهى التشغيل السابق على نحو غير طبيعي؛ المعلومات التشخيصيّة متاحة»
3.2 الأساس الأقوى
إذا بدت متطلّباتك أشبه بهذا:
- تشغيل 24/7
- التحكّم بالأجهزة، أو المراقبة، أو التشغيل المقيم
- استخدام كثيف لـ COM / P/Invoke / native SDKs
- عمليّات فرعيّة، أو plugins، أو تنفيذ scripts
- «يجب ألّا يبقى متعطّلاً في بيئات العملاء»
عندئذٍ يستحقّ عادةً الفصل بين:
- العملية العاملة (worker process) للعمل الرئيسي
- launcher / watchdog / service للإشراف وتسجيل الخروج وإعادة التشغيل
- WER LocalDumps على جانب العامل
- جمع أدلّة الـ crash إمّا في التشغيل التالي أو من جانب الـ watchdog
هذا شكلٌ أكثر توجّهاً نحو الإنتاج.
4. أفضل الممارسات للسجلّات العادية
إن حاولت الفوز بالسطر الأخير وحده وقت الـ crash، فعادةً تخسر.
ما يُؤتي ثماره فعلاً هو مسار السجلّ العادي مباشرةً قبل الـ crash.
4.1 سجّل للارتباط لاحقاً، لا للجمال الأدبي
كحدّ أدنى، ينبغي أن تتضمّن السجلّات العادية:
- طابع زمني UTC
- الزمن المنقضي منذ بدء العملية
- PID / TID
- اسم التطبيق، الإصدار، رقم البناء، ومُعرّف commit/build
- session ID
- operation ID / job ID / correlation ID
- اسم الـ module / الشاشة / العامل
- التأثير الجانبي الخارجي مباشرةً قبل الحدث
- كتابة ملفّ
- تحديث DB
- إرسال أمر إلى جهاز
- طلب شبكة
- نوع الاستثناء، HRESULT / Win32 error / exception code
- ملخّص آمن لمعاملات الإدخال المهمّة
- المُعرّفات الهدف أو معرّفات الكائنات ما دامت لا تكشف أسراراً
أكثر التنسيقات عمليّةً هي عادةً:
- JSON Lines
- أو نمط key=value بحدث واحد لكلّ سطر
النثر الطويل أقلّ فائدة من إتاحة الارتباط بين ثلاثة ملفّات أدلّة مختلفة لاحقاً.
4.2 أفرغ أحداث الحدود الحرجة عن قصد
جعل كلّ إدخال سجلّ متزامناً تماماً غالباً ما يكون مكلفاً.
لكن دفع كلّ شيء عبر buffer غير متزامن يعني أنّ المقطع الأخير المهمّ قد يختفي مع الـ crash.
لذا، تقسيم عملي هو:
- الأحداث الإعلاميّة الصغيرة قد تبقى مُخزّنة مؤقّتاً
Warningوما فوق ينبغي أن تُفرَّغ في وقت أبكر- أحداث الحدود الحرجة ينبغي أن تُكتب بشكل متزامن
أحداث الحدود الحرجة النموذجيّة تشمل:
ProcessStartConfigLoadedWorkerStartedExternalCommandSentTransactionCommittedRecoveryStartedFatalPathEntered
الفكرة بسيطة:
حدود العمل والنظام المهمّة ينبغي أن تُسقط على القرص عمداً.
4.3 أبقِ السجلّ العادي وعلامة الـ crash النهائية منفصلَين
هذا أهمّ ممّا يبدو لأوّل وهلة.
إن حاولت وضع كلّ شيء في سجلّ متدرّج واحد، تظهر مشكلات كهذه:
- التدوير حدث في الوقت الخاطئ
- ما زالت الـ async queue تحتجز الأحداث الأخيرة
- مات الـ logger نفسه فور وقوع الاستثناء
- اقتُطع السطر النهائي في منتصفه
لذا، الأكثر أماناً هو الاحتفاظ بمنتجَين منفصلَين على الأقلّ:
app-<session>.jsonl
السجلّ الزمني العاديfatal-last.logأوfatal-<session>.log
ملفّ مُخصّص فقط لعلامة الـ crash النهائية
مجرّد جعل «إلى أين يجب أن يذهب السطر الأخير» صريحاً يساعد كثيراً عمليّاً.
4.4 أبقِ وجهة وقت الـ crash محلّيّة، لا قائمة على الشبكة
الاعتماد على مسار UNC، أو NAS، أو endpoint عبر HTTP، أو cloud API وقت الـ crash محفوفٌ بالمخاطر لأنّ كلّ هذه قد تتداخل:
- فقدان شبكة عابر
- تأخير DNS
- بيانات اعتماد منتهية الصلاحية
- انتظار الـ UI thread
- مشكلات صلاحيات حساب الخدمة
في وقت الـ crash، اكتب إلى مسار محلّي ثابت أوّلاً.
أرسل أو ارفع الدليل فقط بعد إعادة التشغيل أو من عملية أخرى.
4.5 ضع هويّة جلسة في اسم الملفّ
التاريخ وحده لا يكفي حين يُعاد تشغيل التطبيق عدّة مرّات في اليوم.
نمط تسمية عملي يُشبه:
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
القدرة على الإجابة عن «إلى أيّ مثيل تشغيل ينتمي هذا؟» تُسرّع التحقيق بشكل كبير.
5. أفضل الممارسات لعلامة الـ crash النهائية
هذا ليس المكان لبناء logger كامل المزايا.
هذا هو المكان لكتابة سطر قصير واحد، مرّةً واحدة، بأكبر موثوقية ممكنة.
5.1 الغرض ليس التشخيص الكامل؛ بل نقطة دخول مستقرّة
ينبغي أن تحتوي علامة الـ crash النهائية على مجموعة معلومات منتقاة بدقّة:
- وقت الفشل بـ UTC
- PID / TID
- session ID
- الإصدار / رقم البناء
- من أيّ خطّاف أتيت
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- نوع الاستثناء أو exception code
- رسالة قصيرة إن كان من الآمن إصدار واحدة
- آخر operation ID
- اسم ملفّ السجلّ العادي
- مجلّد الـ dump المتوقّع
هذا يكفي.
5.2 أشياء لا ينبغي أن تفعلها في معالج crash
هذه شديدة الاحتمال أن تصبح فخاخاً:
- حلّ logger من حاوية DI
- استخدام async / await
- إطلاق tasks
- الانتظار على الأقفال
- بناء JSON معقّد
- لمس كائنات COM
- إظهار UI dialogs
- ضغط ملفّات
- إرسال إخطارات HTTP / SMTP / Slack / Teams
- تحليل الـ dump داخل العملية
- ابتلاع الاستثناء والاستمرار
معالج الـ crash ليس مجرّد استمرار للتدفّق التحكّمي العادي.
انحز به نحو «اكتب الحدّ الأدنى محلّيّاً وتوقّف.»
5.3 ما ينبغي أن يفعله معالج الـ crash
ينبغي أن يبقى التسلسل بسيطاً بقسوة:
- منع إعادة الدخول
- كتابة سجلّ قصير واحد
- تفريغه
- الإنهاء
إن أمكن، استخدم:
- مجلّداً مُنشأً مسبقاً
- مساراً تمّ التحقّق من وجوده مسبقاً
- موقعاً تمّ التحقّق من ACLs الخاصّة به مسبقاً
بخلاف السجلّات العادية، حجم العلامة القاتلة منخفض جدّاً بحيث من المعقول تفريغه بقوّة.
5.4 لا تحاول إبقاء العملية حيّةً
للاستثناءات غير المتوقّعة الناتجة عن أخطاء المبرمج، الأكثر أماناً عادةً معاملة المعالج النهائي كـ جهاز تسجيل، لا كـ جهاز استعادة.
خصوصاً إن وقع الفشل:
- في منتصف تحديثات الحالة المشتركة
- على الـ UI thread
- في حلقة مراقبة أو حلقة أصل
- بصيغة
AccessViolationException - بصيغة
StackOverflowException - عبر حدود native
- عبر مسارات invalid-parameter / purecall / terminate في الـ CRT
غريزة عدم الـ crash مفهومة، لكنّ عمليّةً نصف معطوبة كثيراً ما تكون أسوأ تشغيليّاً وتشخيصيّاً من عمليّة منهية بنظافة.
6. تحذيرات خاصّة بالأطر
6.1 .NET بشكل عامّ: AppDomain.CurrentDomain.UnhandledException
هذا مفيد كنقطة إخطار نهائية.
لكن نمط الاستخدام الأكثر أماناً ما زال:
- اكتب علامة الـ crash النهائية
- اختياريّاً، سجّل رسالة دنيا واحدة في Windows Event Log
- لا تستمرّ
- لا تقم بحلقات انتظار أو إعادة محاولة هناك
عاملها كآخر إخطار، لا كمكان تصبح فيه العملية صحّيّة من جديد.
6.2 WinForms: Application.ThreadException
هذا صعب لأنّه قد يجعل التطبيق يبدو وكأنّه يستمرّ على الـ UI thread.
قد يكون ذلك مقبولاً لحالات أخطاء UI متوقّعة معالَجة صراحةً، لكنّه عادةً أساس سيّئ لـ الأعطال غير المتوقّعة الناتجة عن أخطاء المبرمج.
إذا كانت جودة التحقيق تهمّ أكثر من وهم النجاة، فالأكثر أماناً عادةً:
- تسجيل الحدّ الأدنى من الدليل
- أو الانحياز نحو
UnhandledExceptionMode.ThrowException - ثمّ الإنهاء وحفظ السجلّات والـ dump
6.3 WPF: Application.DispatcherUnhandledException
في WPF نفس الإغراء:
- يستهدف استثناءات الـ UI thread
Handled = trueيجعل الاستمرار الظاهري ممكناً- لكنّ الحالة قد تتباعد بسهولة بين الشاشة وداخليّات التطبيق
لذا في WPF أيضاً، الأكثر أماناً غالباً استخدامه كـ نقطة دخول للتسجيل، لا كآليّة دعم حياتيّ.
6.4 لا تجعل TaskScheduler.UnobservedTaskException مسارك الأساسيّ
هذا ليس مسار «سطر الـ crash النهائي» الخاصّ بك.
هو مفيد لاكتشاف أخطاء مراقبة استثناءات الـ task، خصوصاً أثناء التطوير، لكنّه ضعيف كآليّة أدلّة crash أساسيّة.
استخدمه لإظهار أخطاء التصميم، لا كعمود فقري لتسجيل الـ crash الرئيسي.
6.5 native Win32 / C++: لا تثق كثيراً بـ SetUnhandledExceptionFilter
في الكود الـ native، من المغري جدّاً توقّع الكثير من SetUnhandledExceptionFilter.
لكنّه ما زال يعمل في سياق الـ thread الفاشل، وقد يتأثّر بـ:
- stack غير صالح
- recursion عميق
- حالة heap معطوبة بالفعل
- أقفال محتجزة عند نقطة الـ crash
لذا الأفضل معاملته كـ:
خطّاف إخطار نهائي يبذل أقصى جهده، لا آليّة استعادة عامّة
6.6 ينبغي أن يُغطّي C++ الـ native أيضاً مسارات إنهاء CRT / C++ runtime
إذا نظرت فقط إلى SEH غير المعالَج، تفوتك مسارات إنهاء مهمّة.
عمليّاً، تريد أيضاً التفكير في أشياء مثل:
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
تُمثّل هذه مسارات إنهاء بمستوى الـ runtime من جانب C أو C++ runtime.
النمط الآمن ما زال:
- اكتب علامة crash دنيا هناك أيضاً
- تجنّب العمل الثقيل
- أنهِ
- دع WER / dumps يحملان الدليل الرئيسي
7. استخدم WER LocalDumps كأساس
هذا أحد أقوى الخيارات العمليّة على Windows.
7.1 التوصية الأولى: WER LocalDumps
من حيث ترك دليل ذي معنى بعد crash بموثوقية لا بأس بها، عادةً ما تكون WER LocalDumps أفضل أداة أولى.
الأسباب بسيطة:
- يمكن لنظام التشغيل أن يترك الـ dump
- من السهل إدخالها بدون أدوات إضافيّة
- يمكن تكوينها لكلّ تطبيق
- تنقل قطعة الـ crash الرئيسيّة خارج العملية الفاشلة
وبخلاف السجلّات العاديّة، يستطيع الـ dumps الإجابة عن أسئلة مثل:
- أيّ thread فشل
- كيف كان شكل الـ stack
- أين كانت حدود الـ module
- هل المشكلة المحتملة managed أم native أم COM أم متعلّقة بـ SDK
7.2 التكوين النموذجي
مثلاً، لتخزين dumps لـ 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 تحقّق دائماً من ACLs مجلّد الـ dump
تماماً كالسجلّات، الـ dumps عديمة الفائدة إن كانت العملية لا تستطيع الكتابة في المجلّد المُكوَّن.
هذا يهمّ خصوصاً لـ:
- خدمات Windows
- العمليّات الفرعيّة بفصل صلاحيات
- الحسابات المُقيّدة على أجهزة الميدان
- التخطيطات المتعلّقة بـ UAC
لذا ينبغي أن تكون وجهة الـ dump:
- مُنشأة مسبقاً
- مختبَرة الكتابة
- محدودة بعدد احتفاظ
- يمكن الوصول إليها تشغيليّاً لمن يحتاج فعلاً استرداد الملفّ
7.4 إن أردت إرفاق ملفّات سجلّ بتقارير WER
إن كنت تستخدم تدفّق تقارير WER من Microsoft أو عمليّاتك الخاصّة المبنيّة على WER، فإنّ WerRegisterFile يمكن أن يساعد في تسجيل ملفّات السجلّ الحاليّة كقطع تقرير ذات صلة.
لكن ينبغي معاملة هذا كـ مسار إضافي، لا بديلاً للحفظ المحلّي.
ترتيب الأولويات العملي عادةً:
- السجلّ العادي المحلّي
- العلامة القاتلة المحلّية
- الـ dump المحلّي
- تسجيل الملفّ ذي الصلة على جانب WER اختياريّاً
7.5 حافظ على انضباط الإصدارات والرموز مع الـ dumps
إن جمعت dumps واكتشفت لاحقاً أنّك لا تملك:
- ملفّ EXE / DLL المطابق
- ملفّ PDB المطابق
- مُعرّف البناء
فإنّ الـ dump يصبح أضعف بكثير.
كحدّ أدنى، احفظ:
- الـ binaries المنشورة
- ملفّات PDBs المطابقة
- الإصدار
- طابع زمن البناء
- مُعرّف commit/build
- مُعرّف الـ installer أو الحزمة
ينبغي معاملة جمع الـ dumps والاحتفاظ بالرموز كوحدة تشغيليّة واحدة.
8. كيف نُفكّر في MiniDumpWriteDump ومُبلّغي الـ crash المُخصّصين
ثمّة حالات حقيقيّة يكون فيها العمل المُخصّص منطقيّاً:
- تريد مسار UI «احفظ التشخيصات»
- تريد تجميع السجلّات وملفّات الإعداد
- لديك عدّة عمليّات فرعيّة
- تريد إخفاء قيم مُخصّصاً قبل الرفع
لكنّ القاعدة الأهمّ تبقى:
لا تجعل العملية المتعطّلة تحمل الكثير من عمل الـ dump أيضاً.
8.1 فضّل إنشاء dump خارج العملية على dump ذاتي
MiniDumpWriteDump قويّ، لكنّه عادةً أكثر أماناً حين يُستدعى من عملية أخرى بدلاً من العملية المتعطّلة نفسها.
شكل شائع:
- يكتشف العامل مساراً قاتلاً إن أمكن
- يُخطر مساعداً عبر event، أو named pipe، أو آليّة بسيطة أخرى
- يُنشئ المساعد الـ dump للعامل
- يُجمّع المساعد ذيل السجلّ وملفّات الإعداد
- يُدرج المساعد الدليل في طابور للرفع لاحقاً
بهذه الطريقة، يبقى المساعد سليماً حتى إن لم يكن العامل كذلك.
8.2 إن وجب البقاء داخل العملية، انحز نحو thread dump مُخصّص
إن استحالت عملية منفصلة، يمكن أن يكون thread dump مُخصّص أفضل من منطق thread فاشل اعتباطي.
لكن حتى عندئذٍ، تبقى النتيجة best effort.
منطق dump مُخصّص لا يحوّل سحريّاً معالجة الـ crash إلى مسار مضمون.
8.3 انقل العمل الثقيل إلى البدء التالي أو إلى جانب المساعد
أشياء يُغري المُبلّغون المُخصّصون كثيراً بفعلها وقت الـ crash تشمل:
- ضغط zip
- تلخيص يدرك الرموز
- رفع إلى الخادم
- التقاط لقطة شاشة
- استعلامات قاعدة بيانات لمزيد من السياق
كلّ ذلك أكثر أماناً عادةً بعد إعادة التشغيل أو من جانب المساعد، لا وقت الـ crash.
9. ما الذي يتغيّر حين تُضيف عملية watchdog
للأنظمة الطويلة التشغيل، يساعد watchdog أو مُشرف كثيراً.
9.1 ما يستطيع الـ watchdog تسجيله
watchdog / launcher / parent service يستطيع حفظ أشياء مثل:
- وقت بدء العملية الفرعيّة
- معاملات البدء
- PID
- الإصدار المراقَب
- وقت آخر heartbeat
- وقت الخروج
- exit code
- عدد إعادات التشغيل
- ما إذا كان dump موجوداً
- ما إذا حدثت إعادة تشغيل
مجرّد ذلك يُخبرك بوضوح أكبر بكثير عن:
- ما إذا كان فعلاً crash
- ما إذا كان نظام التشغيل قيد الإيقاف
- ما إذا أغلقه المستخدم
- ما إذا قُتل بعد تعليق
- ما إذا دخل حلقة إعادة تشغيل
9.2 متى يستحقّ ذلك خصوصاً
يكون جذّاباً خصوصاً حين يكون لديك:
- عامل يلفّ SDKs من الموردين
- معالجة صور، أو معالجة فيديو، أو إدخال/إخراج جهاز
- حلقة أصل تراقب أو تستطلع
- تنفيذ scripts أو plugins
- استضافة legacy لـ COM / ActiveX
- جسور 32-bit / 64-bit أو حدود تكاملية كثيفة أخرى
وضع الجزء الخطر في عامل مُخصّص يجعل تصميم كلّ من أدلّة الـ crash وسياسة إعادة التشغيل أسهل بكثير.
10. أنماط مضادّة شائعة
10.1 catch (Exception) -> log -> continue
شائع، وخطر.
كثيراً ما يُؤدّي إلى:
- تغييرات جزئيّة متروكة
- حالة مشتركة معطوبة
- إخفاقات ثانوية
- سبب جذري ضبابي
تحصل على سطر سجلّ إضافي، لكن غالباً بثمن حادثة أطول بكثير.
10.2 الثقة بطابور الـ logger غير المتزامن وحده
التسجيل غير المتزامن نفسه ليس سيّئاً.
المشكلة حين يُضيف المسار القاتل أيضاً إلى الطابور ويعود.
إن مات العامل فوراً، يموت الطابور معه.
ينبغي أن يكون للمسار القاتل مسار هروب للكتابة المباشرة.
10.3 الرفع من معالج الـ crash
هذا مغرٍ، لكنّه محفوف بالمخاطر لأنّه يجرّ معه:
- DNS
- TLS
- proxies
- المصادقة
- timeouts
- انتظارات إعادة المحاولة
نفّذ الإرسال بعد إعادة التشغيل بدلاً من ذلك.
10.4 توجد dumps، لكنّها لا ترتبط بالسجلّات العاديّة
هذا شائع أيضاً.
- اسم ملفّ الـ dump لا يحمل هويّة جلسة
- السجلّ العادي لا يحمل PID أو session
- سجلّ الـ watchdog لا يحمل PID
- أرقام البناء غير متطابقة
النتيجة أنّ تيّارات الأدلّة الثلاثة تبدو كقصص مختلفة.
10.5 استخدام أحداث الاستثناء غير المعالَج في WinForms / WPF كدعم حياتي
في البداية يبدو هذا جذّاباً، لأنّ التطبيق يبدو وكأنّه «توقّف عن الـ crash».
لكن في الواقع كثيراً ما يُنشئ حالات zombie مثل:
- ما زالت الشاشة موجودة
- منطق العامل ميّت
- ما زالت الـ UI تعرض أزراراً نشطة
- لا أحد يعرف ما إذا حدث الحفظ فعلاً
10.6 تجاهل مسارات إنهاء الـ runtime الـ native
إن فكّرت فقط في SetUnhandledExceptionFilter، فقد تفوتك:
- invalid parameter
- purecall
- terminate
- fast fail
تكون تصاميم C++ الـ native أقوى حين تعترف صراحةً أيضاً بمسارات إنهاء CRT / C++ runtime.
11. قائمة تحقّق التنفيذ الأدنى
إن أوفيت بالآتي، يكون التصميم بالفعل عمليّاً تماماً.
- السجلّات العاديّة بحدث واحد لكلّ سطر
- كلّ سجلّ يحمل UTC، PID، TID، الإصدار، والـ session
ProcessStartوProcessExitمُسجَّلان- أحداث الحدود المهمّة تُفرَّغ بشكل متزامن
- يوجد ملفّ علامة crash نهائية مُخصّص
- لا يعتمد المسار القاتل على الـ logger غير المتزامن وحده
- WER LocalDumps مُكوَّنة لكلّ تطبيق
- تمّ التحقّق من ACL مجلّد الـ dump
- PDBs والـ binaries المنشورة محفوظة
- التشغيل التالي يستطيع كشف الإنهاء غير الطبيعي السابق
- الضغط / الرفع / الإخطار يحدث بعد إعادة التشغيل أو من عملية أخرى
- C++ الـ native يُغطّي أيضاً مسارات invalid parameter / purecall / terminate
- قمت عمداً بتعطيل التطبيق في الاختبار وتأكّدت أنّ الدليل يبقى فعلاً
السطر الأخير مهمّ خصوصاً:
التصميم ليس حقيقيّاً حتى تختبر أنّ الدليل يُترك فعلاً.
12. إلى أيّ حدّ نختبر
حالات اختبار موصى بها تشمل:
| الاختبار | ماذا نُؤكّد |
|---|---|
| استثناء managed غير معالج | يظهر السجلّ العادي والعلامة القاتلة والـ dump جميعاً |
| استثناء UI thread | مسارات أحداث WinForms / WPF تعمل كما هو متوقّع |
| استثناء worker thread | يصل إلى المسار العلوي المقصود ويكتشف الـ watchdog الخروج |
| استثناء native | يُجمَع dump WER فعلاً |
| invalid parameter / terminate | إنهاء جانب الـ runtime ما زال يترك الحدّ الأدنى المتوقّع من الدليل |
| قتل قسري | حتى إن فشل التسجيل داخل العملية، يُسجّل الـ watchdog الخروج غير المتوقّع |
| إعادة التشغيل | إخطار التشغيل التالي والجمع وسلوك الرفع تعمل |
المفتاح هو تأكيد:
في ظلّ ظرف الفشل هذا، تبقى هذه الملفّات بالضبط
لا فقط:
«ينبغي أن يُسجَّل شيء ما على الأرجح.»
13. الخاتمة
إن أردت دليلاً كافياً للتحقيق في أعطال أخطاء المبرمج في تطبيقات Windows لاحقاً، فالفكرة الجوهرية بسيطة فعلاً:
- لا تثق بالعملية المتعطّلة وحدها
- وزّع الدليل بين السجلّات العاديّة، وعلامة crash نهائية، وأدلّة نظام التشغيل / عملية أخرى
- أبقِ عمل وقت الـ crash قصيراً ومحلّيّاً
- انقل العمل الثقيل إلى البدء التالي أو عملية أخرى
- استخدم WER LocalDumps كأساس
- انحز نحو سجّل-ثمّ-أنهِ بدلاً من تابع-ثمّ-تأمّل
بمعنى آخر:
بدلاً من محاولة جعل آخر سطر سجلّ واحد بطوليّاً، ابنِ تصميماً يبقى قابلاً للتشخيص حتى إن غاب ذلك السطر الأخير.
ما زلت تريد السطر الأخير، فاحتفظ بعلامة crash نهائية قصيرة في ملفّها الخاصّ.
لكن دع دليل الـ crash الرئيسي يعيش في WER dumps إضافةً إلى السجلّات العاديّة المُؤدّية إلى الفشل.
هذا نمط أكثر استقراراً بكثير في عمل تطبيقات Windows الحقيقي.
مقالات ذات صلة
أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.
جمع crash dumps لتطبيقات Windows: متى تبدأ بـ WER أو ProcDump أو WinDbg
مقدّمة عمليّة لجمع crash dumps لتطبيقات Windows: متى تختار WER LocalDumps أو ProcDump أو MiniDumpWriteDump، وكيف توازن بين mini وfull dum...
المزالق الشائعة في تطوير مكوّنات COM و OCX / ActiveX - فخاخ Visual Studio بين 32-bit و 64-bit، والتسجيل، وصلاحيّات المسؤول
دليل عمليّ يكشف الأسباب الحقيقيّة لإخفاق مكوّنات COM و OCX و ActiveX: عدم تطابق 32-bit / 64-bit مع Visual Studio 2022، وأخطاء regsvr32 و ...
أين يجب التقاط الاستثناءات وتسجيلها ومعالجة الأخطاء - دليل عمليّ للحدود والمسؤوليّات في تسلسل الاستدعاء
دليل عمليّ يساعدك على تحديد مستوى تسلسل الاستدعاء الذي يجب فيه التقاط الاستثناء وكتابة السجلّ وتحويل الإخفاق إلى قرار، مع أمثلة C# وقائمة...
ما هو ClickOnce - كيف يعمل، وكيف تعمل التحديثات، ومتى يلائم العمل الفعليّ ومتى لا يلائمه
نظرة عمليّة على ClickOnce لتوزيع تطبيقات .NET لسطح مكتب Windows: كيف تعمل manifests والتحديثات والـ cache، ومتى يلائم العمل الداخليّ ومتى...
كيف نستعمل Windows Sandbox لتسريع التحقّق من تطبيقات Windows - صلاحيّات المسؤول والبيئات النظيفة وإعادة إنتاج حالات نقص الصلاحيّات أو الموارد
مرشد عمليّ يبيّن كيف يسرّع Windows Sandbox التحقّق من تطبيقات Windows، عبر ملفّات .wsb لكلّ سيناريو وفحوصات المستخدم القياسيّ ومحاكاة شُح...
أين يتصل هذا الموضوع
ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.
التحقيق في الأخطاء وتحليل السبب الجذري
ندعم التحقيق في الأعطال التي يصعب إعادة إنتاجها، والمشكلات بعد التشغيل الطويل، وتوقّفات التواصل.
الملف الشخصي للمؤلف
صفحة الملف الشخصي لمؤلف المقالة.
غو كومورا
مؤسّس شركة كومورا سوفت ذ.م.م.
يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.
روابط عامة