كيف نُبقي سجلّات الأعطال في تطبيقات 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.

ما يعمل أفضل بكثير في الواقع هو تصميم لا يعتمد على العملية المتعطّلة وحدها.

بمعنى آخر، فكّر في ثلاث طبقات:

  1. سجلّات زمنية اعتيادية أثناء التشغيل العادي
  2. علامة crash نهائية مُصغّرة في لحظة الفشل
  3. أدلّة 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 وما فوق ينبغي أن تُفرَّغ في وقت أبكر
  • أحداث الحدود الحرجة ينبغي أن تُكتب بشكل متزامن

أحداث الحدود الحرجة النموذجيّة تشمل:

  • ProcessStart
  • ConfigLoaded
  • WorkerStarted
  • ExternalCommandSent
  • TransactionCommitted
  • RecoveryStarted
  • FatalPathEntered

الفكرة بسيطة:

حدود العمل والنظام المهمّة ينبغي أن تُسقط على القرص عمداً.

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.UnhandledException
    • Application.ThreadException
    • DispatcherUnhandledException
    • SetUnhandledExceptionFilter
    • _set_invalid_parameter_handler
    • set_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

ينبغي أن يبقى التسلسل بسيطاً بقسوة:

  1. منع إعادة الدخول
  2. كتابة سجلّ قصير واحد
  3. تفريغه
  4. الإنهاء

إن أمكن، استخدم:

  • مجلّداً مُنشأً مسبقاً
  • مساراً تمّ التحقّق من وجوده مسبقاً
  • موقعاً تمّ التحقّق من 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_handler
  • set_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 يمكن أن يساعد في تسجيل ملفّات السجلّ الحاليّة كقطع تقرير ذات صلة.

لكن ينبغي معاملة هذا كـ مسار إضافي، لا بديلاً للحفظ المحلّي.

ترتيب الأولويات العملي عادةً:

  1. السجلّ العادي المحلّي
  2. العلامة القاتلة المحلّية
  3. الـ dump المحلّي
  4. تسجيل الملفّ ذي الصلة على جانب 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 الحقيقي.

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

كيف نستعمل Windows Sandbox لتسريع التحقّق من تطبيقات Windows - صلاحيّات المسؤول والبيئات النظيفة وإعادة إنتاج حالات نقص الصلاحيّات أو الموارد

مرشد عمليّ يبيّن كيف يسرّع Windows Sandbox التحقّق من تطبيقات Windows، عبر ملفّات .wsb لكلّ سيناريو وفحوصات المستخدم القياسيّ ومحاكاة شُح...

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة