تحقيق تعطّل الكاميرا الصناعيّة بعد تشغيل طويل: فصل handle leak

· آخر تحديث: · · تطوير Windows, التحقيق في الأعطال, الكاميرا الصناعيّة, handle leak, تصميم السجلّات

سجل التعديلات (3 تحديثات، آخر تحديث 3 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُضيفت روابط الاستشارة الموجودة في الأصل الياباني (consultation_services). ولم يتغيّر نصّ المقالة نفسه. قراءة النسخة السابقة لهذا التحديث (DOI: 10.5281/zenodo.22240810)
أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621331)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

小村 豪 (2026). تحقيق تعطّل الكاميرا الصناعيّة بعد تشغيل طويل: فصل handle leak. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621331 https://comcomponent.com/ar/blog/2026/03/11/002-handle-leak-industrial-camera-long-run-crash-part1/

DOI (أحدث نسخة)
10.5281/zenodo.21621331
DOI (هذه النسخة)
10.5281/zenodo.22279718

عندما يسقط تطبيق Windows فجأة بعد تشغيل طويل فقط، يغري الشكّ أوّلاً في تسريب الذاكرة كثيراً. لكن في الواقع ليس نادراً أن يكون handle leak الجاني الرئيسيّ، ولا يظهر إلا بعد أسابيع كعطل ثانويّ.

ما نقدّمه هنا حالة تحقيق في تطبيق Windows يتحكّم بكاميرا صناعيّة سقط فجأة بعد نحو شهر من التشغيل المتواصل. بالتضييق تبيّن أنّ السبب handle leak كان يحدث على مسار فشل حول إعادة اتّصال الكاميرا.

في الجزء الأوّل نرتّب ما يعنيه handle leak، وكيف ضُيِّق نطاق هذه الظاهرة، وأيّ سجلّات ينبغي إبقاؤها لمنع التكرار. في الجزء الثاني نتناول بنية اختبار مسارات الفشل في بنية اختبار مسارات الفشل في Windows بـ Application Verifier.

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

المحتويات

  1. الخلاصة أوّلاً (في جملة)
  2. ما هو handle leak
    • 2.1. ما نعنيه بـ «handle» هنا
    • 2.2. لماذا يظهر بعد التشغيل الطويل فقط بسهولة
    • 2.3. الفرق عن تسريب الذاكرة
  3. الحالة: تطبيق تحكّم بكاميرا صناعيّة يسقط فجأة بعد شهر
    • 3.1. العَرَض الذي كان يحدث
    • 3.2. أوّل المؤشّرات التي نظرنا إليها
    • 3.3. موضع التسريب الذي كان السبب الحقيقيّ
  4. كيف ضُيِّق النطاق
    • 4.1. اختصار الزمن دون انتظار إعادة إنتاج بوحدة الشهر
    • 4.2. النظر بميل Handle Count
    • 4.3. النظر في تقابل create/open مع close/dispose
    • 4.4. handle leak يبحث عن «مكان التسريب» لا «مكان السقوط»
  5. السجلّات اللازمة لمنع التكرار
    • 5.1. الحدّ الأدنى الذي ينبغي إبقاؤه أوّلاً
    • 5.2. السجلّات التي عُزِّزت فعلاً
    • 5.3. بأيّ حبيبات تُؤخَذ
  6. دليل اختيار تقريبيّ
  7. الخلاصة
  8. روابط مرجعيّة

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 18، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

1. الخلاصة أوّلاً (في جملة)

  • في تطبيق تحكّم يسقط بعد تشغيل طويل فقط، يُنظَر حتماً إلى Handle Count لا إلى Private Bytes وحدها
  • handle leak يختبئ بسهولة لا في المسار العاديّ بل في مسار timeout / reconnect / الفشل في الوسط / early return
  • الصفّ الذي يسقط فعلاً غالباً ليس مكان التسريب، بل المكان الذي صار فيه إنشاء مقبض جديد متعذّراً لاحقاً
  • السجلّات اللازمة أوّلاً: سياق operation/session، وhandle count للعمليّة، وتقابل open/close للمورد، وأخطاء Win32 / HRESULT / SDK
  • تدوير الاتّصال والقطع وإعادة الاتّصال ومسار الفشل آلاف المرّات في حلقة قصيرة أسرع من انتظار إعادة إنتاج بوحدة الشهر
  • Application Verifier الذي نتناوله في الجزء الثاني فعّال جدّاً، لكنّ الأساس قبله التمكّن من تتبّع انهيار lifetime بالسجلّات الذاتيّة

باختصار، ما ينبغي فعله أوّلاً في قضايا كهذه ليس التأمّل في أنّه «سقط بعد مدّة طويلة»، بل جعل زيادة الموارد ومسار الفشل قابلين للرصد.

handle leak غالباً ما يكون قد اتّخذ وجه عطل ثانويّ عندما يُكتشَف. لذلك إذا نُظر إلى استثناء لحظة السقوط وحدها، يسهل المشي في اتجاه بعيد عن الصواب.

تركيب ما ينبغي فعله أوّلاًيبيّن أنّ النظر إلى استثناء لحظة السقوط وحدها يسهّل المشي في اتجاه بعيد عن الصواب، وأنّ جعل زيادة الموارد ومسار الفشل قابلين للرصد أوّلاً يوصل إلى هويّة handle leak الذي اتّخذ وجه عطل ثانويّ.النظر إلى استثناء لحظة السقوط وحدهاالمشي في اتجاه بعيد عن الصوابرصد زيادة الموارد ومسار الفشلالوصول إلى هويّة التسريب

الشكل 1: قبل التأمّل في «واقعة السقوط»، يُجعَل شكل الزيادة ومسار الفشل قابلين للرصد.

2. ما هو handle leak

2.1. ما نعنيه بـ «handle» هنا

المقبض هنا معرّف تشير به عمليّة Windows إلى مورد في نظام التشغيل. ما يدخل في النطاق مثلاً هذه.

التصنيف أمثلة
كائنات النواة event, mutex, semaphore, thread, process, waitable timer
عائلة I/O open لـ file, pipe, socket, device
ما يكثر في التحكّم بالأجهزة event داخليّ في SDK الكاميرا، كائن انتظار مرتبط بتسجيل callback، مقابض مرتبطة بخيط التصوير

ما يصير مشكلة خصوصاً في تطبيقات التحكّم هو نمط «نسيان إغلاق مورد فُتح مؤقّتاً لعمليّة معيّنة، على مسار فشل في الوسط».

التدفّق النموذجيّ هكذا.

  • إنشاء event واحد في كلّ إعادة اتّصال
  • فشل تسجيل callback أو بدء التصوير في الوسط
  • يُغلَق في مسار النجاح، ولا يُغلَق في مسار الفشل
  • الاختبارات القصيرة العاديّة تمرّ بمسار النجاح فقط فيُفوَّت

هذا النوع يختبئ بسهولة في مراجعة الشيفرة وفي التشغيل الفعليّ على حدّ سواء.

النمط النموذجيّ للتسريب على مسار الفشل فقطيبيّن إنشاء event في كلّ إعادة اتّصال، وإغلاقه في مسار النجاح إذا نجح تسجيل callback أو بدء التصوير، وعدم إغلاقه في مسار الفشل في الوسط، وأنّ الاختبارات القصيرة تمرّ بمسار النجاح فقط فتُفوَّت.نجاحفشل في الوسطإنشاء event في كلّ إعادة اتّصالهل نجح التسجيل والبدء؟الإغلاق في مسار النجاحعدم الإغلاق في مسار الفشلالاختبارات القصيرة تمرّ بمسار النجاح فقط فتُفوَّت

الشكل 2: نسيان إغلاق مورد فُتح مؤقّتاً على مسار فشل في الوسط. شكل شائع خصوصاً في تطبيقات التحكّم.

2.2. لماذا يظهر بعد التشغيل الطويل فقط بسهولة

handle leak لا ينكسر بالضرورة بشكل صارخ في مرّة واحدة. ما يزعج أكثر هو تسريب بميل صغير من نوع واحد فقط لكلّ فشل.

تشغيل عاديّtimeout / reconnect أحياناًإنشاء Event Handle على مسار الفشلCloseHandle لا يُستدعىHandle Count يزيد قليلاًالتكرار مئات المرّاتفشل CreateEvent / SDK openتعطّل / توقّف في موضع آخر

الشكل 3: تسريب صغير بواحد لكلّ مرّة يتراكم مئات المرّات على شروط حدّيّة لتشغيل 24/7 فيظهر.

إذا سُرّب واحد فقط لكلّ reconnect، لا يحدث شيء في دقائق. لكن في تطبيق تحكّم بالأجهزة يعمل 24/7 تتكرّر شروط حدّيّة مثل timeout وإعادة التهيئة واستعادة القطع. النتيجة مظهر غريب: لا يظهر إلا بعد أسابيع.

المهمّ هنا أنّ handle leak نفسه لا يصير بالضرورة صفّ التعطّل. الكثير ينكسر بهذا الشكل.

  • تفشل API إنشاء event / file / thread جديد
  • SDK يعجز عن إنشاء المورد الذي يحتاجه داخليّاً فيعيد رمز فشل عامّاً فقط
  • معالجة الخطأ بعد الفشل رقيقة، فيُداس null / invalid handle ويسقط
  • تزيد timeout فينتج عن ذلك قتل من watchdog أو التحكّم الأعلى

أي أنّ موضع التعطّل «آخر ضحيّة»، وليس بالضرورة «أوّل جانٍ».

موضع التعطّل آخر ضحيّةيبيّن أنّ استمرار تسريب المقابض في مكان ما يؤدّي في النهاية إلى فشل API إنشاء مورد جديد، فيظهر كتعطّل أو توقّف في موضع آخر رقيق في معالجة الخطأ، لذا صفّ السقوط ليس أوّل جانٍ بل آخر ضحيّة.استمرار تسريب المقابض في مكان مافشل API إنشاء مورد جديدتعطّل أو توقّف في موضع آخرصفّ السقوط ليس سوى آخر ضحيّة

الشكل 4: التسريب نفسه لا يصير بالضرورة صفّ التعطّل. شكل الانكسار غالباً عطل ثانويّ.

هنا يظهر سؤال ساذج. لماذا يسقط التطبيق ببضعة آلاف من المقابض فقط.

بالأرقام وحدها الحدّ الأقصى بعيد جدّاً. مقابض كائنات النواة حدّها النظريّ لكلّ عمليّة 2^24 (نحو 16.77 مليوناً). لكن المقابض تُوضَع في مجمع الصفحات، فعدد ما يمكن إنشاؤه فعلاً يتحدّد بالذاكرة المتاحة، وعلى Windows 32بت يقلّ كثيراً عن القيمة النظريّة.

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

ما يُضرَب أوّلاً تقدير متى ينفع
كائنات GDI نظريّاً 65,536 لكلّ جلسة. وفوق ذلك حدّ افتراضيّ لكلّ عمليّة يمكن تغييره في نطاق 256 إلى 65,536 بـ GDIProcessHandleQuota في السجلّ تطبيق تشارك فيه الواجهة الرسوميّة. يُضرَب بسهولة عند رتبة آلاف
جدول إدارة داخل SDK حسب البائع امتلاء جدول مقابض أو مصفوفة بطول ثابت داخل SDK الكاميرا أوّلاً
موارد النواة مثل مجمع الصفحات مشتركة على الجهاز كلّه عندما تُستهلك موارد غير المقابض أيضاً معاً
فضاء العنوان الافتراضيّ لعمليّة 32بت 2GB / 3GB المخازن المخصَّصة المرافقة للمقابض تنفع أكثر من المقابض نفسها

أي أنّ قراءة «ما زال هناك هامش حتّى الحدّ الأقصى فلا بأس» لا تصمد. ينبغي النظر لا إلى بلوغ الحدّ الأقصى، بل إلى عودة ما ينبغي أن يعود. عند قيام الميل، من الأأمن اعتبار الأمر شاذاً أصلاً.

ما يُضرَب قبل الحدّ النظريّيبيّن أنّ حالات السقوط ببلوغ الحدّ النظريّ لمقابض النواة أقلّيّة، وأنّ ما يُضرَب أوّلاً فعلاً هو حدّ كائنات GDI أو جدول الإدارة داخل SDK أو فضاء عنوان عمليّة 32بت، لذا ينبغي الحكم بعودة ما ينبغي أن يعود.الحدّ النظريّ للنواة نحو 16.77 مليوناًالبلوغ إلى هناك أقلّيّةما يُضرَب أوّلاً فعلاًحدّ GDIجدول الإدارة داخل SDKفضاء عنوان 32بتالحكم بالعودة أو عدمها

الشكل 5: «ما زال هناك هامش حتّى الحدّ الأقصى» ليس مادّة اطمئنان. يُنظَر إلى الشذوذ عند قيام الميل.

2.3. الفرق عن تسريب الذاكرة

في أعطال ما بعد التشغيل الطويل يغري الشكّ أوّلاً في تسريب الذاكرة. هذا طبيعيّ في ذاته، لكن handle leak قد يكون أسرع إذا نُظر إليه على محور آخر.

الزاوية تسريب الذاكرة handle leak
أوّل مؤشّر يُنظَر إليه Private Bytes, Commit, Working Set Handle Count
العَرَض النموذجيّ ضغط ذاكرة، paging، بطء، OOM فشل Create* / Open* / تهيئة داخل SDK، عطل ثانويّ
أين يختبئ بسهولة ذاكرة مؤقّتة، إبقاء مراجع، نسيان التحرير عدم تناظر create/open مع close/dispose
المظهر زيادة الذاكرة تدريجيّاً زيادة handle count تدريجيّاً دون عودة

لذلك في تضييق التشغيل الطويل يسهل أن يصير «النظر إلى الذاكرة وحدها» قيادة بعين واحدة. النظر إلى Handle Count وThread Count معاً على الأقلّ يرتّب الأمر كثيراً.

مؤشّرات تُنظَر معاً في تضييق التشغيل الطويليبيّن أنّ تسريب الذاكرة وhandle leak يختلف فيهما المؤشّر المنظور، لذا يُنظَر مع مؤشّرات الذاكرة مثل Private Bytes إلى Handle Count وThread Count معاً لتفادي القيادة بعين واحدة.تضييق التشغيل الطويلعائلة الذاكرة (Private Bytes وغيرها)Handle CountThread Countالزيادة دون عودة تشير إلى handle leak

الشكل 6: النظر إلى الذاكرة وحدها قيادة بعين واحدة. أعداد المقابض والخيوط تُتابَع على الشاشة نفسها.

3. الحالة: تطبيق تحكّم بكاميرا صناعيّة يسقط فجأة بعد شهر

3.1. العَرَض الذي كان يحدث

الظاهرة كانت بسيطة.

  • تطبيق Windows يتحكّم بكاميرا صناعيّة يعمل 24/7
  • في الأوقات العاديّة يعمل بشكل عاديّ
  • بعد نحو شهر يسقط التطبيق فجأة في يوم ما
  • بعد إعادة التشغيل يعمل مرّة أخرى لفترة

أوّل ما يزعج هو «طول المدّة حتّى السقوط». انتظار شهر لكلّ إعادة إنتاج قاسٍ جدّاً كتحقيق.

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

بهذا المظهر يمكن الاشتباه أوّلاً في أيّ من التالي.

  • عدم استقرار جانب SDK الكاميرا
  • عطل مؤقّت ناشئ عن الاتّصال أو قطع الجهاز
  • تسريب ذاكرة
  • race حول الخيوط
  • فشل تهيئة لا يظهر في السجلّ

أي أنّ الحالة كانت «ما يبدو مشبوهاً أكثر ممّا يلزم».

نقطتان صعّبتا التحقيق في هذه الحالةيبيّن أنّ السقوط فجأة بعد نحو شهر من التشغيل المتواصل يجعل إعادة الإنتاج تستغرق شهراً لكلّ مرّة، وأنّ موضع السقوط ليس نفسه تماماً في كلّ مرّة، فيتكاثر المرشّحون المشبوهون من SDK والاتّصال والذاكرة وغيرها.سقوط مفاجئ بعد نحو شهرإعادة الإنتاج تستغرق شهراً لكلّ مرّةموضع السقوط يختلف قليلاً في كلّ مرّةحالة مرشّحين مشبوهين أكثر ممّا يلزم

الشكل 7: اجتماع «طول المدّة حتّى السقوط» و«تذبذب موضع السقوط» يمنع التقدّم بالتخمين.

3.2. أوّل المؤشّرات التي نظرنا إليها

لذلك كان أوّل ما فُعل النظر إلى زيادة موارد العمليّة كلّها. في هذه الحالة كان اتجاه الرصد تقريباً هكذا.

المؤشّر الاتجاه المرصود القراءة
Handle Count يزيد قليلاً بعد reconnect أو timeout ولا يعود اشتباه handle leak
Private Bytes فيه صعود وهبوط، لكن ميل الزيادة الأحادية ضعيف الجاني الرئيسيّ ليس بالضرورة heap
Thread Count شبه أفقيّ احتمال تسريب خيوط منخفض
موضع السقوط يختلف قليلاً في كلّ مرّة احتمال عطل ثانويّ مرتفع

عند هذه النقطة ضاق النظر كثيراً. لأنّ الأطبيع كان النظر لا إلى «يسقط بعد شهر» بل إلى «يسرّب شيئاً قليلاً قليلاً في الوسط، فيسقط بعد شهر نتيجة ذلك».

التقدير الذي ضاق من رصد المؤشّرات الأولىيبيّن أنّ رصد زيادة Handle Count دون عودة، وضعف ميل Private Bytes، وأفقيّة Thread Count تقريباً، واختلاف موضع السقوط في كلّ مرّة، ضيّق التقدير إلى تسريب قليل قليل يسقط بعده بعد شهر.Handle Count يزيد ولا يعودضيق النظرميل Private Bytes ضعيفThread Count شبه أفقيّتسريب قليل قليل ثمّ سقوط بعد شهر

الشكل 8: بصفّ أشكال المؤشّرات الأربعة يظهر لا «يسقط بعد شهر» بل «يسرّب طوال الوقت».

3.3. موضع التسريب الذي كان السبب الحقيقيّ

السبب النهائيّ كان نسيان إغلاق event handle أُنشئ على مسار فشل التهيئة عند إعادة اتّصال الكاميرا.

بتبسيط التدفّق يصير هكذا.

SDK الكاميراWindowsتطبيق التحكّمSDK الكاميراWindowsتطبيق التحكّمreturn على مسار الفشلCloseHandle لا يُستدعىloop[إعادة اتّصال متكرّرة]CreateEventتسجيل callbackفشل في الوسط / timeoutHandle Count يزيد قليلاً قليلاًCreateEvent / Open التاليفشلتعطّل كعطل ثانويّ

الشكل 9: السبب الحقيقيّ نسيان إغلاق event handle على مسار فشل إعادة الاتّصال. بعد التراكم يسقط في موضع آخر.

كصورة للشيفرة، التسريب بهذا الشكل.

handle = CreateEvent(...)

if (!RegisterCallback(handle))
{
    return Error;   // CloseHandle(handle) が抜けている
}

if (!StartAcquisition())
{
    return Error;   // ここでも close が抜ける
}

...
CloseHandle(handle)

سبب سهولة تفويته في الاختبار القصير واضح أيضاً جدّاً.

  • الإغلاق يحدث في تشغيل سليم -> إنهاء سليم
  • الفشل يحدث فقط في منتصف reconnect
  • لا اختبار يطأ مسار الفشل ذلك كمّاً كبيراً
  • في الإنتاج يتراكم قليلاً قليلاً على أسابيع

أي أنّ البنية كانت «لا يُرى إذا نُظر إلى المسار العاديّ فقط، لكنّه يتسرّب بسهولة في المسار الشاذّ».

اتّجاه التصحيح ليس صارخاً.

  • تقريب مسؤوليّة create/open من close/dispose
  • الإمالة إلى finally / destructor / كائن الجلسة بحيث يُحرَّر حتماً حتّى عند فشل في الوسط
  • توضيح الملكيّة قبل تسجيل callback وبدء التصوير وبعدهما
  • التعبير عن «من يغلق» بمسؤوليّة الشيفرة لا بالتعليقات
هيكل اتّجاه التصحيحيبيّن اتّجاه التصحيح: تقريب مسؤوليّة create من close، وإمالة التحرير إلى finally أو المدمّر أو كائن الجلسة بحيث يُحرَّر حتماً حتّى عند فشل في الوسط، والتعبير عمّن يغلق بمسؤوليّة الشيفرة لا بالتعليق.اتّجاه التصحيحتقريب مسؤوليّة create من closeإمالة التحرير إلى finally أو المدمّرالتعبير عن الملكيّة بمسؤوليّة الشيفرةعدم الاعتماد على اصطلاح تعليقات

الشكل 10: ليس تصحيحاً صارخاً. عمر المورد يُدمَج في بنية الشيفرة نفسها.

النصّ وحده يصعب فهمه، لذا نضع أيضاً الشكل بعد إعادة كتابة المعالجة نفسها.

في C++ تُعدّ نوع RAII صغير يحمل المقبض، فلا يُترَك HANDLE خام داخل الدالّة.

// C++17 / Windows
#include <windows.h>
#include <utility>

class UniqueHandle
{
public:
    UniqueHandle() noexcept = default;
    explicit UniqueHandle(HANDLE h) noexcept : h_(h) {}

    UniqueHandle(const UniqueHandle&) = delete;
    UniqueHandle& operator=(const UniqueHandle&) = delete;

    UniqueHandle(UniqueHandle&& other) noexcept
        : h_(std::exchange(other.h_, nullptr)) {}

    UniqueHandle& operator=(UniqueHandle&& other) noexcept
    {
        if (this != &other)
        {
            reset(std::exchange(other.h_, nullptr));
        }
        return *this;
    }

    ~UniqueHandle() { reset(); }

    HANDLE get() const noexcept { return h_; }
    explicit operator bool() const noexcept { return h_ != nullptr; }

    void reset(HANDLE h = nullptr) noexcept
    {
        if (h_ != nullptr)
        {
            ::CloseHandle(h_);
        }
        h_ = h;
    }

private:
    HANDLE h_ = nullptr;
};

باستخدام هذا تنتفي الحاجة إلى إضافة CloseHandle على مسار الفشل.

// عضو CameraSession: UniqueHandle frameReady_;
bool CameraSession::Reconnect()
{
    UniqueHandle frameReady{ ::CreateEventW(nullptr, TRUE, FALSE, nullptr) };
    if (!frameReady)
    {
        return false;   // فشل الإنشاء نفسه. لا شيء يُغلق
    }

    if (!RegisterCallback(frameReady.get()))
    {
        return false;   // حتى إن عدت هنا، المدمّر يغلق
    }

    if (!StartAcquisition())
    {
        // الفشل بعد اكتمال التسجيل: ألغِ التسجيل قبل الإغلاق.
        // إن خرجت دون الإلغاء يغلق المدمّر بـ CloseHandle بينما يبقى SDK
        // ممسكاً بالمقبض الذي مُرِّر. في الإطار التالي يُشار إلى رقم محرَّر،
        // وإن أُعيد استخدام ذلك الرقم لمورد آخر يظهر الأمر كـ
        // «حدث غير ذي صلة يُرفع من تلقاء نفسه»
        UnregisterCallback();
        return false;
    }

    // عند النجاح فقط انقل الملكيّة إلى جانب session
    frameReady_ = std::move(frameReady);
    return true;
}

في C# كثيراً ما لا يكفي using دفعة واحدة، لذا يُجعَل «هل أمكن تسليم الملكيّة» علماً، ويُرمى في finally فقط إذا لم يُسلَّم. كتابة using var ببساطة تتلف أيضاً عند النجاح.

// C# / .NET 8
// حقل CameraSession: private ManualResetEvent? _frameReady;
public bool Reconnect()
{
    var frameReady = new ManualResetEvent(false);
    var handedOver = false;
    var registered = false;

    try
    {
        if (!RegisterCallback(frameReady))
        {
            return false;
        }

        registered = true;

        if (!StartAcquisition())
        {
            return false;
        }

        _frameReady?.Dispose();
        _frameReady = frameReady;
        handedOver = true;
        return true;
    }
    finally
    {
        if (!handedOver)
        {
            // قبل الرمي، ألغِ أولاً المرجع الذي يمسكه الخارج.
            // SDK يحتفظ بالمقبض الممرَّر عند التسجيل،
            // فعكس الترتيب يطرق مقبضاً محرَّراً
            if (registered)
            {
                UnregisterCallback();
            }

            frameReady.Dispose();
        }
    }
}

كلاهما يفعل الشيء نفسه. البنية: مهما خرجت من أيّ موضع في الوسط، المورد الذي لم يُحدَّد مالكه يُرمى حتماً. ليس أن يكتب الإنسان «أغلق عند الفشل» في كلّ مرّة، بل أن ينوب النوع وfinally عن ذلك.

من يتولّى التحرير يُقرَّر بتسليم الملكيّةيبيّن البنية: مهما خرجت من أيّ موضع في الوسط، إذا سُلّمت الملكيّة إلى الجلسة تتولّى الجلسة التحرير لاحقاً، وإذا لم تُسلَّم يرمي النوع أو finally حتماً، وقبل الرمي يُلغى التسجيل لدى SDK.سُلّمتلم تُسلَّممهما خرجت من أيّ موضع في الوسطهل سُلّمت الملكيّة؟الجلسة تتولّى التحرير لاحقاًالنوع أو finally يرمي حتماًإلغاء التسجيل قبل الرمي

الشكل 11: لا تُكتَب «أغلق عند الفشل» في كلّ مرّة، بل يُقرَّر من يتولّى التحرير بوجهة الملكيّة.

هذا ترتيب يدمج عمر المورد في الشيفرة أكثر ممّا هو تقنيّة خاصّة.

4. كيف ضُيِّق النطاق

من هذا الفصل تظهر عبارات إنجليزيّة للتحقيق كما هي. نضع أوّلاً ملاحظة ترجمة قصيرة.

المصطلح بالعربية تقريباً المعنى في هذا المقال
baseline قيمة أساس القيمة عند الاستقرار بعد انتهاء الإحماء. يُنظَر إلى الفرق منها
leakSlope ميل التسريب كم زاد لكلّ دورة. مؤشّر ذاتيّ لسرعة الزيادة
structured log سجلّ منظَّم سجلّ يحدّد بنوداً بشكل key=value لا كجمل. يمكن تجميعه آليّاً لاحقاً
heartbeat تقرير دوريّ سجلّ يستمرّ في إخراج تأكيد البقاء وقيم الموارد على فترات ثابتة
harness إطار اختبار خارجيّ برنامج تنفيذ صغير يكرّر المعالجة المراد تجربتها بدلاً من التطبيق الأصليّ
phase طور علامة لأيّ مرحلة من المعالجة نقف فيها الآن، مثل OpenStart وReconnectStart

4.1. اختصار الزمن دون انتظار إعادة إنتاج بوحدة الشهر

في تحقيق كهذا، انتظار شهر في كلّ مرّة مسار سيّئ. ما ينبغي فعله هو المرور بالمسار المشبوه مرّات كثيرة في وقت قصير.

في هذه الحالة ضغطنا إعادة الإنتاج بتدوير حلقة كهذه.

نعملاالتشغيلopen للكاميرابدء التصويرtimeout / قطع محاكىإعادة الاتّصالاستئناف التصويرالتكرار N مرّةتأكيد الفرق عند الإنهاء

الشكل 12: دون انتظار إعادة إنتاج بوحدة الشهر، تُدار حدود open والقطع وإعادة الاتّصال فقط آلاف المرّات في حلقة قصيرة.

النقطة أن الوقت يُصرَف في عمليّات عمر الحدود، لا في زمن «التصوير جارٍ» العاديّ.

السيناريوهات التي تنفع عمليّاً كهذه.

  • تدوير open -> start -> stop -> close كمّاً كبيراً
  • إحداث timeout عمداً وتدوير reconnect
  • إفشال الأمر فور تسجيل callback
  • إدخال قطع أثناء القطع، وقطع أثناء إعادة الاتّصال، وتسابق shutdown

لا يلزم إعادة إنتاج شهر من التشغيل الفعليّ بإتقان. بالأحرى، وطء حافة lifetime المشبوهة آلاف المرّات أقرب كثيراً إلى السبب.

4.2. النظر بميل Handle Count

قبل ذلك نكتب أين يُنظَر إلى Handle Count. إن لم يُعرَف هذا، يصير الفصل كلّه وعوداً على ورق.

الوسيلة العملية المشهد المناسب
مدير المهامّ فتح علامة «التفاصيل»، والنقر الأيمن على عنوان العمود → «اختيار الأعمدة» → تأشير «المقابض» رؤية العدد الآن فوراً
Process Explorer اختيار العمليّة وفتح الخصائص، والنظر إلى Handle Count في علامة Process Performance. ترتيب عرض Handles في الجزء الأسفل حسب Type يُظهر أيضاً التفصيل حسب النوع معرفة أيّ نوع من المقابض يزيد
handle.exe أخذ تجميع حسب النوع كنصّ بـ handle -s -p CameraApp إبقاء رصد نقطيّ في السجلّ
PowerShell Get-Process -Name CameraApp \| Select-Object Name, Id, HandleCount جلب دوريّ بالنصّ البرمجيّ
typeperf typeperf "\Process(CameraApp)\Handle Count" -si 60 -sc 1440 -o handles.csv تسجيل طويل كما هو في CSV
التطبيق نفسه دسّ GetProcessHandleCount أو Process.HandleCount في سجلّ heartbeat استرداد السجلّ فقط من جهاز الإنتاج

طريقة تتبّع التفصيل حسب النوع وزيادة الأحداث بلا اسم مرتّبة كإجراء في Process Explorer / Handle / VMMap في العمل.

في تحقيق التشغيل الطويل، الرهان الحقيقيّ هو الصفّ الأخير «ما يخرجه التطبيق نفسه». مراقبة مدير المهامّ بالعين لا تدوم 24/7.

في تحقيق handle leak، النظر إلى القيمة المطلقة وحدها قد يصعب الفهم. المهمّ هل يعود بعد عمليّة ينبغي أن يعود بعدها، وكم يزيد بعد كم عمليّة.

كطريقة نظر، الترتيب التالي تقريباً أوضح.

  1. تحديد baseline بعد الإحماء
  2. تسجيل Handle Count بعد reconnect / start-stop / close
  3. النظر إلى الفرق لكلّ دورة
  4. النظر أيضاً إلى الميل على عدّة دورات

مثلاً بهذه القراءة.

leakSlope =
    (currentHandleCount - baselineHandleCount)
    / reconnectCount

هل القيمة المطلقة 2000 كثيرة أم قليلة يتذبذب حسب التطبيق. لكن +1 لكلّ reconnect دون عودة شبهة قويّة جدّاً.

نكتب أيضاً تقدير كيف ينبغي أن يبدو المسار السليم. الأرقام نفسها حسب التطبيق، لذا يُحكَم بالشكل.

  • يزيد فور التشغيل. هنا لا نقرأ
  • بعد انتهاء الإحماء ينبغي أن يصير شكلاً يدخل نطاقاً ثابتاً ويخرج منه مع الزيادة والنقصان حسب العمليّة
  • بعد دورة open -> start -> stop -> close واحدة، العودة إلى قيمة شبه مساوية لما قبل الدورة سليم
  • إذا بقي الفرق عن baseline ضمن بضعة بعد 100 دورة، فالأمر سليم أوّلاً
  • بالمقابل، إذا ارتفع الكتف الأيمن نظيفاً بنسبة عدد الدورات، فما يقابل ذلك الميل يتسرّب في كلّ مرّة

ما يُنظَر إليه ليس «كثير أم قليل» بل يعود أم لا يعود. إذا التُبس هذا تُهدَر وقت في الاشتباه بتطبيق سليم.

طريقة قراءة ميل Handle Countيبيّن طريقة القراءة: تحديد baseline بعد الإحماء، والنظر إلى الفرق لكلّ دورة، والحكم بالسلامة أوّلاً إذا عاد بعد الدورة، وبالتسريب في كلّ مرّة بقدر الميل إذا ارتفع الكتف الأيمن بنسبة عدد الدورات.يعودارتفاع كتف أيمن بنسبة الدوراتتحديد baseline بعد الإحماءالنظر إلى الفرق لكلّ دورةهل يعود بعد الدورة؟سليم أوّلاًتسريب في كلّ مرّة بقدر الميل

الشكل 13: لا يُحكَم بكثرة القيمة المطلقة أو قلّتها، بل بشكل «يعود أم لا يعود».

الحيلة هنا ألا يُنظَر إلى Handle Count وحده، بل أن تُدرَج معه على الأقلّ التالية.

  • Handle Count
  • Private Bytes
  • Thread Count
  • ReconnectCount
  • في أيّ phase نقف الآن

بهذا يتّضح سريعاً جدّاً «هل الذاكرة تزيد» و«هل الخيوط تزيد» و«هل المورد لا يعود في كلّ إعادة اتّصال».

4.3. النظر في تقابل create/open مع close/dispose

حتّى لو صار Handle Count للعمليّة كلّها مشبوهاً، ذلك وحده لا يصل إلى موضع التسريب. التالي اللازم هو سجلّ ينظر إلى دورة حياة المورد أزواجاً.

كصورة، structured log كهذا.

CameraSession session=421 cameraId=CAM01 phase=ReconnectStart reason=FrameTimeout handleCount=1824 privateBytesMB=418

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Create osHandle=0x00000ABC handleCount=1825

CameraResource session=421 resourceId=evt-884 kind=Event name=FrameReady action=Close osHandle=0x00000ABC handleCount=1824

المهمّ هنا ألا يُعتمَد على osHandle وحده. قيمة المقبض في Windows قد تُعاد استخدامها لاحقاً، لذا في السجلّ يُفضَّل حمل التالي على الأقلّ لتسهيل التتبّع.

  • sessionId
  • resourceId
  • kind
  • action(Create/Open/Register/Close/Dispose/Unregister)
  • osHandle
  • phase

بهذا يسهل إيجاد تدفّق برئة واحدة: Create موجود وClose غائب.

سجلّ يتتبّع دورة حياة المورد أزواجاًيبيّن أنّ تسجيل Create وClose أزواجاً وربط المورد نفسه بـ sessionId وresourceId يمكّن من اكتشاف تدفّق برئة واحدة فيه Create بلا Close. ويبيّن أيضاً أنّ osHandle يُعاد استخدامه فلا يكفي وحده للتتبّع.تسجيل Create وClose أزواجاًالربط بـ sessionId وresourceIdاكتشاف Create لم يُغلَقosHandle يُعاد استخدامه فلا يكفي وحده

الشكل 14: للنزول من عدد العمليّة كلّها إلى موضع التسريب يلزم سجلّ يجعل دورة حياة المورد أزواجاً.

4.4. handle leak يبحث عن «مكان التسريب» لا «مكان السقوط»

هذه النقطة مهمّة جدّاً.

handle leak كثيراً ما يبدو بهذا الشكل.

  • صفّ السقوط: فشل CreateEvent
  • التسريب الحقيقيّ: غياب CloseHandle على مسار الفشل منذ أيّام

أي أنّ آخر API سقطت هي مخرج الضرر، وليست بالضرورة مدخل السبب.

لذلك كترتيب تحقيق،

  1. النظر إلى أيّ مورد يستمرّ في الزيادة
  2. النظر إلى أيّ حدّ عمليّة لا يعود عنده
  3. البحث عن المواضع التي انهار فيها تقابل create/open مع close/dispose
  4. أخيراً قراءة موضع التعطّل

هذا الترتيب يقلّل الضياع كثيراً.

ترتيب التحقيق للوصول إلى مكان التسريبيبيّن أنّ النظر إلى المورد الذي يستمرّ في الزيادة، ثمّ حدّ العمليّة الذي لا يعود عنده، ثمّ البحث عن انهيار تقابل create وclose، وأخيراً قراءة موضع التعطّل، ترتيب يقلّل الضياع.النظر إلى المورد الذي يستمرّ في الزيادةالنظر إلى حدّ العمليّة الذي لا يعودالبحث عن انهيار تقابل create وcloseأخيراً قراءة موضع التعطّل

الشكل 15: موضع التعطّل ليس سوى مخرج. يُتتبَّع من المدخل وهو «مكان التسريب».

5. السجلّات اللازمة لمنع التكرار

5.1. الحدّ الأدنى الذي ينبغي إبقاؤه أوّلاً

ما نفع في هذا التحقيق ليس زيادة كمّيّة السجلّات وحدها. كان زيادة معلومات منظَّمة يمكن الوصول بها إلى السبب لاحقاً.

في الحدّ الأدنى يُراد إبقاء التالي.

التصنيف البنود المطلوبة في الحدّ الأدنى السبب
سياق العمليّة cameraId, sessionId, operationId, reconnectCount, phase لربط في أيّ مرّة من أيّ عمليّة حدث الأمر
موارد العمليّة handleCount, privateBytes, workingSet, threadCount لتضييق ما يزيد أوّلاً
دورة حياة المورد action, resourceId, kind, osHandle, owner لتتبّع تقابل create/open مع close/dispose
نتيجة الاستدعاء الخارجيّ win32Error, HRESULT, sdkError, timeoutMs لمقارنة نوع الفشل لاحقاً
انتقال الحالة OpenStart, OpenDone, ReconnectStart, ReconnectDone, ShutdownStart وغيرها لمعرفة في منتصف أيّ phase انهار الأمر
بيئة التنفيذ pid, tid, buildVersion, machineName لمطابقة dump / الرموز / الموزَّع

لا نقول إنّ هذا كافٍ. لكن بغيابه على الأقلّ يسهل أن يصير السجلّ لا يُبقي سوى واقعة «سقط».

5.2. السجلّات التي عُزِّزت فعلاً

في هذه الحالة عُزِّزت السجلّات في الاتّجاهات التالية.

  1. heartbeat دوريّ
    • إخراج Handle Count / Private Bytes / Thread Count / ReconnectCount كلّ 1 إلى 5 دقائق
  2. سجلّ حدود بوحدة جلسة الكاميرا
    • OpenStart
    • CallbackRegistered
    • AcquisitionStart
    • TimeoutDetected
    • ReconnectStart
    • ReconnectDone
    • CloseStart
    • CloseDone
  3. سجلّ دورة حياة المورد
    • Create/Open/Register وClose/Dispose/Unregister لـ event / thread / file / timer / رمز تسجيل SDK
  4. تطبيع الأخطاء
    • عدم الاكتفاء برسالة الاستثناء، وإخراج win32Error وHRESULT وsdkError وphase معاً

المهمّ عدم تغيير نوع السجلّ بين النجاح والفشل. إذا صار الشكل مختلفاً عند الشذوذ فقط، يصعب التجميع لاحقاً.

أربع منظومات سجلّ عُزِّزتيبيّن أنّ جمع أربع منظومات — heartbeat دوريّ، وسجلّ حدود بوحدة جلسة الكاميرا، وسجلّ دورة حياة المورد، وتطبيع الأخطاء — دون تغيير النوع بين النجاح والفشل يصير سجلاً يمكن الوصول به إلى السبب لاحقاً.heartbeat دوريّ (قيم الموارد)سجلّ يمكن الوصول به إلى السببسجلّ حدود الجلسةسجلّ دورة حياة الموردتطبيع الأخطاءعدم تغيير النوع بين النجاح والفشل

الشكل 16: ليست زيادة كمّيّة السجلّات، بل اصطفاف أربع منظومات يمكن مطابقتها لاحقاً.

5.3. بأيّ حبيبات تُؤخَذ

ما يسهل فعله هنا «إخراج الكلّ بـ INFO مؤقّتاً». لكن ذلك يصنع جدار سجلّات عند القراءة لاحقاً. هذا متعب جدّاً.

كحبيبات، التقسيم التالي تقريباً واقعيّ.

  • رصد دوريّ
    • Handle Count, Private Bytes, Thread Count, ReconnectCount
  • حدود العمليّة
    • start / done / fail للجلسة
  • حدود المورد
    • create/open/register وclose/dispose/unregister
  • تفاصيل عند الشذوذ
    • رمز الخطأ، المكدّس، محفّز أخذ dump

السجلّ التفصيليّ لكلّ إطار عادة غير لازم. بالأحرى، السجلّ الذي يُقرأ فيه «أيّ مسؤوليّة فُتحت، وأيّ مسؤوليّة أُغلقت» أنفع في أعطال المدد الطويلة.

تقسيم حبيبات السجلّيبيّن التقسيم: الرصد الدوريّ أعداد الموارد، وحدود العمليّة بدء الجلسة ونهايتها، وحدود المورد تقابل create وclose، والتفاصيل العميقة عند الشذوذ فقط، وعدم صنع جدار سجلّات بإخراج الكلّ بـ INFO.حبيبات السجلّرصد دوريّ: الأعدادحدود العمليّة وحدود الموردتفاصيل عميقة عند الشذوذ فقطإخراج الكلّ بـ INFOجدار سجلّات لا يُقرأ

الشكل 17: حبيبات تُقرأ فيها «من فتح ومن أغلق» أوفق من تفاصيل كلّ إطار.

6. دليل اختيار تقريبيّ

  • سقوط بعد أيّام إلى أسابيع فقط
    • إدخال heartbeat لـ Handle Count / Private Bytes / Thread Count أوّلاً
  • وجود retry / reconnect / shutdown
    • صنع harness يدوّر تلك الحدود وحدها كمّاً كبيراً أوّلاً
  • استخدام كثير لـ native SDK / P/Invoke / Win32
    • قيمة تطبيق Application Verifier في الجزء الثاني مرتفعة
  • مشاركة الواجهة الرسوميّة أيضاً
    • يُفضَّل النظر أيضاً إلى GDI Objects / USER Objects فوق Handle Count
  • استثناء لحظة السقوط وحده لا يبيّن شيئاً
    • ترتيب structured log لـ operation / session / resource lifecycle أوّلاً أسرع

البند الأخير مهمّ جدّاً. في تحقيق الأعطال كثيراً ما يحسم الأمر هل جُعل قابلاً للرصد أكثر من تقنيّة التحليل نفسها.

7. الخلاصة

في تطبيق يسقط بعد تشغيل طويل فقط، يُنظَر إلى Handle Count لا إلى الذاكرة وحدها. handle leak يختبئ بسهولة لا في المسار العاديّ بل في مسار الفشل الشاذّ، وموضع التعطّل غالباً ليس موضع التسريب بل مخرج العطل الثانويّ. كقراءة للعَرَض، الأمر يؤول في النهاية إلى هذه الثلاث.

لمنع التكرار، تُقرَّب مسؤوليّة create/open من close/dispose، ويُبقى سجلّ يحمل سياقاً بوحدة session / operation، ويُسجَّل مورد العمليّة ودورة حياة المورد كلاهما. في الاختبار تُدار timeout / reconnect / shutdown في حلقة قصيرة دون انتظار إعادة إنتاج بوحدة الشهر، ويُجعَل شرط القبول لا «عدم الانكسار» فقط بل «إمكان التتبّع عند الانكسار». ما نفع هنا هو هذا الجمع. في الجزء الثاني نُظهر بـ Application Verifier أشكال انكسار صعبة الظهور مثل نقص الذاكرة وشذوذ المقابض في وقت أبكر.

في تطبيقات التحكّم، مرور المسار السليم مهمّ، لكن «معرفة ما حدث» عند الانكسار تنفع كثيراً في التشغيل الطويل.

handle leak عطل من النوع الذي ينفع فيه هذا الفرق تماماً. بدل النظر عند لحظة الحدوث فقط، النظر بالزيادة والحدود وتقابل المسؤوليّات يسهّل التتبّع كثيراً.

الجزء الثاني: بنية اختبار مسارات الفشل في Windows بـ Application Verifier

8. روابط مرجعيّة

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

صفحة دراسة حالة توضّح بنية مشابهة للتشخيص أو تحديد الأولويات أو إعادة التصميم.

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

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما هو handle leak؟
نسيان عمليّة Windows إغلاق المقابض التي تشير بها إلى موارد نظام التشغيل مثل event وmutex وfile وsocket، فيستمر Handle Count في الزيادة. النمط الشائع خصوصاً نسيان إغلاق مورد فُتح مؤقّتاً لعمليّة معيّنة على مسار فشل في الوسط مثل timeout أو reconnect أو early return. الاختبارات القصيرة العاديّة تمرّ بمسار النجاح فقط فيسهل تفويته.
كيف نميّز تسريب الذاكرة عن handle leak؟
المؤشّر المنظور مختلف. تسريب الذاكرة يزيد فيه Private Bytes أو Commit تدريجيّاً، أمّا handle leak فيزيد فيه Handle Count تدريجيّاً ولا يعود. في تضييق التشغيل الطويل، النظر إلى الذاكرة وحدها يصير قيادة بعين واحدة، لذا الأساس النظر أيضاً إلى Handle Count وThread Count معاً. إذا كانت الواجهة الرسوميّة مشاركة يُنظَر أيضاً إلى GDI Objects / USER Objects.
لماذا يسقط التطبيق بعد تشغيل طويل فقط إذا وُجد handle leak؟
الميل الصغير الذي يسرّب واحداً فقط لكلّ فشل لا يحدث شيئاً في دقائق، لكن في تشغيل 24/7 تتكرّر شروط حدّيّة مثل timeout وإعادة الاتّصال فتتراكم على أسابيع. في النهاية يظهر كعطل ثانويّ عندما تفشل API إنشاء event/file/thread جديد. المهمّ أيضاً أنّ موضع التعطّل غالباً ليس مكان التسريب بل آخر ضحيّة.
كيف نحقّق في handle leak؟
لا ننتظر إعادة إنتاج بوحدة الشهر. نضغط إعادة الإنتاج بتدوير حدود عمليّات العمر المشبوهة آلاف المرّات في حلقة قصيرة، مثل open -> start -> stop -> close أو timeout وreconnect. بعد الإحماء نحدّد baseline وننظر إلى فرق Handle Count لكلّ دورة وإلى الميل، ونبحث بـ structured log يحمل sessionId وresourceId وaction المواضع التي انهار فيها تقابل create/open مع close/dispose، وأخيراً نقرأ موضع التعطّل. هذا الترتيب يقلّل الضياع.

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

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

غو كومورا

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

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

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