دليل عملي لـ FileSystemWatcher: معالجة الفقد والتكرار

· آخر تحديث: · · FileSystemWatcher, C#, .NET, تطوير Windows, تكامل الملفّات, التصميم

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

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

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

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

小村 豪 (2026). دليل عملي لـ FileSystemWatcher: معالجة الفقد والتكرار. شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621323 https://comcomponent.com/ar/blog/2026/03/10/000-filesystemwatcher-safe-basics/

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

FileSystemWatcher هو أوّل API يخطر بالبال عند مراقبة تغيّرات الملفّات في .NET على Windows. يمكن استقبال إنشاء الملفّات والأدلّة وتغييرها وحذفها وإعادة تسميتها كأحداث، وهذا مريح. لكن إذا عومل Created أو Changed كأنّه إشعار اكتمال، تقع الحوادث بسهولة: فقد، وإشعارات مكرّرة، وقراءة ملفّ ما زال يُكتَب.

يرتب هذا المقال استخدام FileSystemWatcher ونقاط الحذر، على افتراض تكامل ملفّات بـ .NET على Windows أساساً. فكرة القفل الحصريّ التي يقوم عليها يمكن الرجوع إليها أيضاً في أساسيّات القفل الحصريّ في تكامل الملفّات - أفضل ممارسات قفل الملفّ وclaim الذرّي.

في الواقع قد يُطلَق Created أثناء نسخ الملفّ قبل اكتماله، وChanged لا يأتي بالضرورة مرّة واحدة. إذا تركزّت التغيّرات في وقت قصير قد يفيض المخزن الداخليّ فتفوت التغيّرات الفرديّة.

لذلك فإنّ عمود التصميم هو هذا.

  • الإشعار مجرّد محفّز
  • الحقيقة إعادة مسح الدليل
  • الملكيّة تُؤخَذ بـ claim ذرّي
  • في النهاية تُستوعَب الازدواجيّة بـ idempotency

في المتن نمرّ بالترتيب على المزالق عند إدخال FileSystemWatcher في تكامل الملفّات وفق هذا التفكير.

الشيفرة الواردة هنا منشورة على GitHub كمجموعة عيّنات يمكن بناؤها وتشغيلها: مكتبة، وعرض console يعمل على دليل مؤقّت، واختبارات وحدة تنشئ ملفّات وتغيّرها فعليّاً للتحقّق من الأحداث.

filesystemwatcher-safe-basics - komurasoft-blog-samples (GitHub)

الجمهور المستهدف والافتراضات

المقال موجّه للمطوّرين الذين يكتبون بـ .NET على Windows معالجة تراقب دليل استقبال وتلتقط الملفّات. أمثلة الشيفرة تفترض C# / .NET 8 فما فوق، لكن الفكرة نفسها لا تتقيّد بلغة.

يستخدم المقال المصطلحات نفسها الواردة في المقال السابق المرتبط أعلاه (القفل الحصريّ في تكامل الملفّات). كلمات مثل claim وidempotency وmanifest وbundle تظهر من الفصل 4 فصاعداً بلا شرح إضافيّ، لذلك نلخّصها أوّلاً سطراً سطراً حتّى يمكن المتابعة دون قراءة المقال السابق.

مصطلحات يجدر ضبطها أوّلاً

المصطلح المعنى
claim أخذ ملكيّة بمعنى «أنا من يعالج هذا الملفّ»، بشكل لا يسمح لعامل آخر بالتداخل. في التنفيذ يُستخدَم rename من incoming/ إلى processing/<worker>/، وتصير العمليّة الوحيدة التي نجح rename لديها هي المالكة (4.3)
idempotency (idempotence) خاصّيّة أنّ معالجة الهدف نفسه مرّتين أو أكثر تعطي النتيجة نفسها لمعالجته مرّة واحدة. لأنّ التكرار وإعادة المسح مفترضان مسبقاً، هذا ما يستوعب الأمر في النهاية (4.5)
manifest ملفّ صغير يصف المحتوى، يوضع بجانب بيانات الأصل. إذا وُضعت فيه أعداد السجلات وhash وIdempotencyKey وما شابه، يستطيع الجانب المستقبل أن يقرّر هل عولج من قبل
bundle وحدة تجمّع عمليّة تكامل واحدة. إذا وُضع الأصل + manifest + ملفّات مساعدة في دليل واحد، يمكن أخذ claim للدليل كلّه بـ rename واحد (4.3)
full rescan عدم الاعتماد على الأحداث، بل تعداد دليل المراقبة من الصفر من جديد، وإعادة فرز ما يجوز معالجته (4.4)
overflow فيضان المخزن الداخليّ لـ FileSystemWatcher وفقد الإشعارات الفرديّة. يُبلَّغ عنه بحدث Error (2.3)
ready حالة حُكم فيها بأنّ «القراءة صارت مباحة». لا تُخمَّن، بل تُحكَم بوجود الاسم النهائيّ أو done / manifest (4.2)

المحتويات

  1. الخلاصة أوّلاً (في جملة)
    • 1.1. أصغر شيفرة تعمل أوّلاً
  2. أنماط سوء الفهم الشائعة عند استخدام FileSystemWatcher (رسوم)
    • 2.1. اعتبار Created إشعار اكتمال
    • 2.2. الثقة بعدد أحداث Changed وترتيبها
    • 2.3. فقد التغيّرات بفيضان المخزن الداخليّ
  3. الأنماط المضادّة
    • 3.1. المعالجة مباشرة داخل معالج الحدث
    • 3.2. محاولة استعادة الحالة الحقيقيّة من تسلسل الأحداث
    • 3.3. اعتبار التوقّف عن Changed اكتمالاً
    • 3.4. الاعتقاد أنّ رفع InternalBufferSize حلّ المشكلة
    • 3.5. تسجيل Error ثمّ تجاهله
  4. أفضل الممارسات
    • 4.1. طيّ الإشعارات إلى «طلب إعادة مسح»
    • 4.2. التصريح بشرط الاكتمال في الجانب المرسل
    • 4.3. أخذ claim ذرّيّاً في الجانب المستقبل
    • 4.4. إجراء full rescan عند البدء وoverflow وإعادة الاتّصال
    • 4.5. افتراض idempotency
  5. شيفرة شبه كاذبة (مقتطفات)
    • 5.1. نمط فشل نموذجيّ
    • 5.2. مثال في الاتجاه الصحيح (بصياغة حرّة)
  6. دليل اختيار تقريبيّ
  7. الخلاصة
  8. روابط مرجعيّة

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

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

  • أحداث FileSystemWatcher ليست إشعار اكتمال، بل أمارة على تغيّر
  • Created / Changed / Renamed قد تتكرّر، وقد تأتي بترتيب غير متوقّع، وقد تُفقَد عند overflow
  • من الأثبت ألا يُجرى عمل ثقيل في معالج الحدث، بل يُكدَّس طلب إعادة مسح فقط
  • الحكم على الاكتمال يُصرَّح به أساساً عبر temp -> close -> rename / replace أو done / manifest
  • إذا وُجد أكثر من عامل، يلزم أخذ claim ذرّيّاً قبل القراءة
  • ضبط InternalBufferSize مساعد. ما ينفع في النهاية هو full rescan وidempotency

باختصار: لا تعامل FileSystemWatcher كـ «تيار تاريخ حقيقيّ». الإشعار يبقى إشارة «حان وقت النظر»، هكذا يصعب أن ينكسر النظام.

عمود التصميم في هذا المقاليبيّن عمود التصميم في المقال: الإشعار يبقى محفّزاً، والحقيقة تُؤكَّد بإعادة مسح الدليل، والملكيّة تُؤخَذ بـ claim ذرّي، وفي النهاية تُستوعَب الازدواجيّة بـ idempotency.الإشعار محفّزالحقيقة إعادة مسح الدليلالملكيّة claim ذرّيفي النهاية تُستوعَب بـ idempotency

الشكل 1: عمود التصميم. لا تُعامل الأحداث كتاريخ حقيقيّ، بل تبقى إشارة «حان وقت النظر».

1.1. أصغر شيفرة تعمل أوّلاً

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

// C# / .NET 8 コンソールアプリ。通知が届くことを確認するだけの最小形
using System.IO;

using var watcher = new FileSystemWatcher(@"C:\incoming")
{
    Filter = "*.csv",
    NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite,
};

watcher.Created += (_, e) => Console.WriteLine($"Created: {e.FullPath}");
watcher.Changed += (_, e) => Console.WriteLine($"Changed: {e.FullPath}");
watcher.Renamed += (_, e) => Console.WriteLine($"Renamed: {e.OldFullPath} -> {e.FullPath}");
watcher.Error += (_, e) => Console.WriteLine($"Error: {e.GetException().Message}");

watcher.EnableRaisingEvents = true; // ここで監視が始まる
Console.WriteLine("اضغط Enter للخروج");
Console.ReadLine();

حتّى في الحدّ الأدنى، ثلاث نقاط تمنع الحيرة إن ضُبطت أوّلاً.

  • لا يأتي أيّ حدث حتّى يصير EnableRaisingEvents = true. تسجيل المعالجات وحده لا يحرّك شيئاً
  • عمر watcher هو عمر التطبيق. إذا انتهى نطاق المتغيّر المحلّي ودُمِّر، تتوقّف الإشعارات هناك. للإبقاء عليه مقيماً يُحفَظ في حقل أو مكان يعيش طويلاً
  • القيمة الافتراضيّة لـ NotifyFilter هي تركيب LastWrite | FileName | DirectoryName (FileSystemWatcher.NotifyFilter Property في 8. روابط مرجعيّة). التصريح بما يُلتقَط أوضح عند العودة لاحقاً

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

2. أنماط سوء الفهم الشائعة عند استخدام FileSystemWatcher (رسوم)

2.1. اعتبار Created إشعار اكتمال

هذا أوضح لغم. في النسخ أو النقل يُطلَق Created في لحظة إنشاء الملفّ، ثم قد يتبعه Changed مرّة أو أكثر.

الجانب المستقبلFileSystemWatcherwatched dirالجانب المرسلالجانب المستقبلFileSystemWatcherwatched dirالجانب المرسلالنسخ ما زال جارياًنقص أسطر / JSON تالف / ZIP تالفإنشاء orders.csvCreatedOnCreatedفتح orders.csv والقراءةكتابة الباقيChangedChanged

الشكل 2: Created يُطلَق حتّى أثناء النسخ. القراءة عند الوصول تمسك بيانات مكسورة.

Created قد يعني «ظهر الاسم»، لكنّه لا يضمن «صارت القراءة مباحة». إذا عومل المعنيان كأنّهما واحد، تُعاد مزالق القسم 2.1 من المقال السابق من مسار آخر.

2.2. الثقة بعدد أحداث Changed وترتيبها

Changed لا يأتي مرّة واحدة بالضرورة. حتّى عمليّات عاديّة مثل النقل أو الحفظ قد تظهر منقسمة إلى عدّة أحداث. وقد يُلتقَط أيضاً ما تلمسه برامج مكافحة الفيروسات أو المفهرِس.

FileSystemWatcherAV / indexerwatched dirالتطبيق الذي يحفظFileSystemWatcherAV / indexerwatched dirالتطبيق الذي يحفظليس بالضرورة مرّة واحدة وبهذا الترتيببدء حفظ report.xlsxCreatedChangedrename من ملفّ مؤقّتRenamedChangedفحص / قراءة سماتChanged

الشكل 3: حتّى الحفظ العاديّ ينقسم إلى أحداث متعدّدة، ويختلط ما تلمسه عمليّات خارجيّة. العدد والترتيب لا يُعتمد عليهما.

توقّع «إذا جاء Changed مرّة فقد اكتمل» أو «بعد Renamed لن يُلمَس الملفّ» هشّ جدّاً.

ملاحظات:

  • قد يُطلَق Changed عند rename للملفّ
  • RenamedEventArgs.Name قد يصير null إذا تعذّر على نظام التشغيل ربط old/new
  • الملفّات المخفيّة لا تُتجاهَل. «اسم temp مخفيّ فلن يُرى» لا يصمد
  • إعادة تسمية الدليل المراقب نفسه لا تُشعَر

2.3. فقد التغيّرات بفيضان المخزن الداخليّ

لـ FileSystemWatcher مخزن داخليّ. إذا تركزّت التغيّرات في وقت قصير يفيض هذا المخزن فتفوت الإشعارات الفرديّة.

نعملاتغيّرات كثيرة في وقت قصيرالإشعارات تتراكم في المخزن الداخليّهل يلحق المعالجة؟معالجة الأحداث فرداً فرداًoverflowحدث Errorعدم الوثوق بسلامة السجلّ الفرديّfull rescan للدليل

الشكل 4: إذا تجاوز اندفاع الإشعارات المخزن الداخليّ يحدث overflow، وتنهار سلامة تسلسل الأحداث الفرديّة.

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

3. الأنماط المضادّة

3.1. المعالجة مباشرة داخل معالج الحدث

هذا تحميل مفرط للحكم على الاكتمال وأخذ الملكيّة على الحدث.

watcher.Created += (_, e) =>
{
    using var stream = File.OpenRead(e.FullPath);
    Import(stream); // まだコピー中かもしれない
};

watcher.Error += (_, e) =>
{
    Console.WriteLine(e.GetException()); // 出すだけ
};

المشكلتان اثنتان.

  • عند Created قد يكون المحتوى غير مكتمل
  • لا يوجد تعافٍ من الفشل أو overflow

معالج الحدث يكفي أن يضع طلب إعادة مسح ويعود فوراً. إذا بدأ هنا I/O ثقيل أو تحديث قاعدة بيانات، يخنق نفسه عند الاندفاع.

الحدّ الفاصل لثقل معالج الحدثيبيّن أنّ الأفضل في معالج الحدث وضع طلب إعادة مسح والعودة فوراً، وأنّ بدء I/O ثقيل أو تحديث قاعدة بيانات داخل المعالج يحمل خطر قراءة محتوى غير مكتمل وعدم اللحاق بالمعالجة عند الاندفاع.معالج الحدثوضع طلب إعادة مسح والعودة فوراًI/O ثقيل أو تحديث قاعدة داخل المعالجخطر قراءة محتوى غير مكتملعدم اللحاق بالمعالجة عند الاندفاع

الشكل 5: المعالج يبقى خفيفاً. لا تُحمَّل الحكم على الاكتمال وأخذ الملكيّة على الحدث.

3.2. محاولة استعادة الحالة الحقيقيّة من تسلسل الأحداث

تصميم «أضف إلى القاموس عند Created، حدّث عند Changed، احذف عند Deleted، بدّل المفتاح عند Renamed» يبدو أنيقاً للوهلة الأولى. لكن مع التكرار والتقسيم وoverflow والاضطراب الخارجيّ يتفكّك الاتّساق تدريجيّاً.

switch (e.ChangeType)
{
    case WatcherChangeTypes.Created:
        state[e.FullPath] = Pending;
        break;
    case WatcherChangeTypes.Changed:
        state[e.FullPath] = Modified;
        break;
    case WatcherChangeTypes.Deleted:
        state.Remove(e.FullPath);
        break;
}

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

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

الشكل 6: الهدف ليس إعادة تمثيل تاريخ الأحداث، بل إيجاد ما يجوز معالجته الآن.

3.3. اعتبار التوقّف عن Changed اكتمالاً

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

if (lastChangedAt + TimeSpan.FromSeconds(10) < DateTime.UtcNow)
{
    return Ready;
}

هذا يتعثّر في حالات مثل هذه.

  • توقّف نسخ ملفّ كبير مؤقّتاً في منتصف الطريق
  • التطبيق المرسل يحفظ على عدّة مراحل
  • الإشعار يبدو متأخّراً على مشاركة شبكيّة
  • عمليّة خارجيّة تعيد كتابة سمات أو أوقات لاحقاً

الاكتمال أثبت إذا صُرِّح به بدل التخمين.

هشاشة تخمين الاكتمال من السكونيبيّن أنّ تخمين الاكتمال من توقّف Changed يخطئ عند توقّف النسخ مؤقّتاً أو الحفظ متعدّد المراحل أو تأخّر الإشعار أو إعادة كتابة السمات لاحقاً، وأنّ تصريح الجانب المرسل بالاكتمال أثبت.تخمين الاكتمال من توقّف Changedخطأ الحكم عند توقّف النسخ مؤقّتاًخطأ الحكم عند الحفظ المتعدّد أو تأخّر الإشعارالجانب المرسل يصرّح بالاكتمالاستقرار بلا اعتماد على التخمين

الشكل 7: السكون ليس دليلاً على الاكتمال. الاكتمال يُقرَّر بالتصريح لا بالتخمين.

3.4. الاعتقاد أنّ رفع InternalBufferSize حلّ المشكلة

ضبط InternalBufferSize مهمّ، لكنّه ليس جسم التصميم.

  • القيمة الافتراضيّة 8192 بايت
  • لا يمكن النزول دون 4096 بايت، ولا تجاوز 64 KB
  • المخزن يستخدم non-paged memory، لذا الزيادة ليست رخيصة كلّما كبرت

أي أنّ رفعه إلى 64 KB لا ينقذ إذا تجاوز اندفاع الإشعارات ذلك. وفوق ذلك، مسألة هل الإشعار إشعار اكتمال أم لا لا تُحَلّ مليمتراً واحداً.

قبل تكبير المخزن، هناك ما يستحقّ المعالجة أوّلاً.

  • تضييق الهدف بـ Filter / Filters
  • جعل NotifyFilter بالحدّ الأدنى اللازم
  • عدم جعل IncludeSubdirectories يساوي true جزافاً
  • تخفيف معالج الحدث
  • إدخال full rescan وidempotency
ما يُفعَل قبل توسيع المخزنيبيّن الترتيب: حتّى رفع InternalBufferSize إلى 64KB لا يمنع الفقد إذا تجاوز الاندفاع ذلك، لذا يُضيَّق الهدف أوّلاً بـ Filter وNotifyFilter، ويُخفَّف المعالج، ويُدخَل full rescan وidempotency.ما يُعالَج أوّلاًالتضييق بـ Filter وNotifyFilterتخفيف المعالجfull rescan وidempotencyضبط InternalBufferSizeيبقى مساعداً أخيراً

الشكل 8: توسيع المخزن ليس جسم التصميم. التضييق وآليّات التعافي تأتي أوّلاً.

3.5. تسجيل Error ثمّ تجاهله

Error ليس إشعاراً من نوع «يظهر أحياناً فلا بأس». overflow المخزن أو فشل استمرار المراقبة يظهر هنا.

watcher.Error += (_, e) =>
{
    _logger.LogError(e.GetException(), "watcher error");
    // ここで終わると、取りこぼしに気づいたのに回復しない
};

في الحدّ الأدنى يُراد هذا القدر.

  • طلب full rescan
  • إذا صار استمرار المراقبة مشكوكاً فيه، النظر في إعادة إنشاء watcher
  • التمكّن من إعادة المعالجة بـ idempotent على افتراض الفقد

4. أفضل الممارسات

4.1. طيّ الإشعارات إلى «طلب إعادة مسح»

ربط Created / Changed / Deleted / Renamed / Error كلٍّ بمعالجة أعمال منفصلة يضعف الرؤية. أوّلاً تُطوى كلّها إلى إشارة واحدة من نوع «انظر».

Created / Changed / Deleted / Renamedscan requestError / overflowstartupإعادة مسح الدليلتعداد المرشّحين readyمحاولة claim

الشكل 9: كلّ إشعار وكلّ startup يُطوى إلى نوع واحد من scan request، ثم تُبحث المرشّحات ready بالمسح ويُجرَّب claim.

نقاط التنفيذ:

  • في معالج الحدث يكفي ضبط dirty = true وإطلاق إشارة
  • المسح يُجمَع في عامل واحد
  • عند الاندفاع تُجمَّع نحو 100 إلى 300ms ثم تُمسَح مرّة واحدة
  • إذا جاءت إشعارات إضافيّة أثناء المسح، تُمسَح مرّة أخرى بعد انتهائه

قيمة 100 إلى 300ms في النقطة الثالثة ليست رقماً له سند في مواصفة أو وثيقة رسميّة، بل قيمة ابتدائيّة من خبرة تشغيل الكاتب. في الواقع أوثق أن تُقاس الاثنتان التاليتان ثم تُقرَّر.

ما يُنظَر إليه كيف يُقرَّر
زمن مسحة واحدة إذا كان الانتظار أقصر من هذا، تتراكم طلبات المسح التالية قبل انتهاء المسحة. يُتَّخذ زمن المسح نفسه أو أكثر كحدّ أدنى تقريبيّ
تأخّر الكشف المقبول زمن الانتظار يصير تأخّر الكشف كما هو. إذا وُجد شرط «المعالجة خلال n ثانية من الوضع»، يُوضَع حدّ أعلى يدخل في جزء من ذلك

مثلاً، إذا انتهت مسحة واحدة في 50ms وكان الكشف خلال ثانية كافياً، فإنّ 100 إلى 300ms تقع في المدى. بالمقابل، إذا كثرت الملفّات واستغرقت المسحة ثواني، فمراجعة بناء المسح (تضييق الهدف، النظر إلى done فقط، تقسيم الأدلّة الفرعيّة) أنفع من إطالة الانتظار.

بهذا، سواء جاءت الأحداث 5 مرّات أو 50، ما يُفعَل في النهاية يتوحّد: «انظر إلى الموجود وابحث عمّا هو ready».

4.2. التصريح بشرط الاكتمال في الجانب المرسل

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

الطريق الملكيّ ما زال هذا.

  • كتابة المحتوى كلّه تحت اسم temp
  • close
  • rename / replace على نظام الملفّات نفسه
  • وضع done / manifest أخيراً عند الحاجة
كتابة المحتوى كلّه في data.tmpflush / closerename / replace إلى data.csvوضع data.done / manifest.jsonالجانب المستقبل ينظر إلى الاسم النهائيّ أو done فقط

الشكل 10: الجانب المرسل يكتب المحتوى كلّه في temp ثم close، وينشر بـ rename، ويضع done / manifest أخيراً عند الحاجة.

هذا هو نفسه في المقال السابق، وهو ما ينفع حقّاً. FileSystemWatcher ليس أداة تخترع الاكتمال، بل أداة تجد الاكتمال المصرَّح به مبكّراً. هذا الفهم أنسب.

4.3. أخذ claim ذرّيّاً في الجانب المستقبل

حتّى لو وُجد مرشّح ready في إعادة المسح، القراءة مباشرة تسمح لعدّة عمّال بالإمساك به معاً. لذلك يُؤخَذ claim ذرّيّاً قبل المعالجة.

processing/worker2processing/worker1incomingscannerprocessing/worker2processing/worker1incomingscannerمن نجح أوّلاً وحده يملك الملكيّةاكتشاف order-123rename order-123rename order-123

الشكل 11: حتّى لو وجد عدّة عمّال المرشّح نفسه، من ينجح في rename وحده يملك الملكيّة.

كما في المقال السابق، rename من incoming إلى processing/<worker>/ واضح. خصوصاً إذا جُمع الأصل + manifest + ملفّات مساعدة في دليل واحد، يمكن أخذ claim بوحدة bundle بسهولة.

incoming/
  order-123/
    payload.csv
    manifest.json

بهذا يكفي rename واحد لدليل bundle لأخذ الملكيّة.

4.4. إجراء full rescan عند البدء وoverflow وإعادة الاتّصال

هذا مهمّ جدّاً.

  • الملفّات الموضوعة قبل تشغيل التطبيق لا تُلتقَط بالأحداث
  • إذا حدث overflow، يصعب الوثوق بتسلسل الأحداث الفرديّة
  • إذا دخلت مشاركة شبكيّة أو انقطاع مؤقّت، من الأأمن افتراض أنّ «شيئاً في تلك الفترة» سقط

لذلك يُفضَّل إدخال full rescan على الأقلّ في هذه الأوقات.

  • عند البدء
  • عند استقبال Error
  • فور إعادة إنشاء watcher
  • على فترات ثابتة كتأمين دوريّ

الفكرة هنا: «watcher تلميح للفروق، وإعادة المسح استعادة للاتّساق».

أوقات إدخال full rescanيبيّن إدخال full rescan في أربعة أوقات: عند البدء، وعند استقبال Error، وفور إعادة إنشاء watcher، وعلى فترات ثابتة كتأمين دوريّ، لاستعادة التغيّرات التي لا تُلتقَط بالأحداث.عند البدءfull rescanعند استقبال Errorفور إعادة إنشاء watcherكتأمين دوريّاستعادة الاتّساق

الشكل 12: watcher تلميح للفروق، وfull rescan استعادة للاتّساق. يُدخَل حتماً في هذه الأوقات الأربعة.

4.5. افتراض idempotency

باستخدام FileSystemWatcher ستُزار الأهداف نفسها أكثر من مرّة. هذا ليس خطأ، وقبوله كتصميم أثبت.

عمليّاً بهذا الشكل.

  • وضع IdempotencyKey في manifest
  • عدم إعادة تنفيذ الآثار الجانبيّة إذا عولج من قبل
  • التمكّن من مطابقة ما أُرشف / سُجّل في قاعدة البيانات / أُرسل
  • جعل full rescan يعني فقط «النظر إلى الشيء نفسه بأمان مرّة أخرى»

صنع exactly-once بالأحداث وحدها متعب جدّاً. قبول at-least-once وإغلاق النهاية بـ idempotency أقوى في العمل.

استيعاب التكرار كمسلَّمةيبيّن قبول زيارة الهدف نفسه أكثر من مرّة كتصميم لا كخطأ، ومطابقة ما عولج عبر IdempotencyKey في manifest وعدم إعادة تنفيذ الآثار الجانبيّة، فيصير النظر آمناً حتّى مع إعادة المسح.زيارة الهدف نفسه أكثر من مرّةقبوله كتصميممطابقة المعالَج عبر IdempotencyKeyعدم إعادة تنفيذ الآثار الجانبيّةالنظر الآمن فقط حتّى مع full rescan

الشكل 13: لا يُصنَع exactly-once بالأحداث، بل يُقبَل at-least-once ويُغلَق بـ idempotency.

5. شيفرة شبه كاذبة (مقتطفات)

5.1. نمط فشل نموذجيّ

using var watcher = new FileSystemWatcher(incomingDir)
{
    Filter = "*.csv",
    IncludeSubdirectories = false,
    EnableRaisingEvents = true,
    InternalBufferSize = 64 * 1024
};

watcher.Created += (_, e) =>
{
    // Created = 完了通知、と思い込んでいる
    ProcessFile(e.FullPath);
};

watcher.Changed += (_, e) =>
{
    // 何度も来るので、とりあえずもう一回処理
    ProcessFile(e.FullPath);
};

watcher.Error += (_, e) =>
{
    Console.WriteLine(e.GetException());
    // 回復しない
};

المشكلات أربع.

  • ربط Created / Changed مباشرة بمعالجة الأعمال
  • لا حكم على الاكتمال
  • لا full rescan عند overflow
  • لا آليّة توقف المعالجة مهما تكرّر الملفّ نفسه

5.2. مثال في الاتجاه الصحيح (بصياغة حرّة)

private readonly SemaphoreSlim _scanSignal = new(0, int.MaxValue);
private int _scanRequested = 0;
private int _fullRescanRequested = 0;

void OnAnyChange(object? sender, FileSystemEventArgs e)
{
    RequestScan(full: false);
}

void OnRenamed(object? sender, RenamedEventArgs e)
{
    RequestScan(full: false);
}

void OnError(object? sender, ErrorEventArgs e)
{
    Log(e.GetException());
    RequestScan(full: true);
}

void RequestScan(bool full)
{
    if (full)
    {
        Interlocked.Exchange(ref _fullRescanRequested, 1);
    }

    if (Interlocked.Exchange(ref _scanRequested, 1) == 0)
    {
        _scanSignal.Release();
    }
}

async Task ScannerLoopAsync(CancellationToken cancellationToken)
{
    RequestScan(full: true); // startup scan

    while (!cancellationToken.IsCancellationRequested)
    {
        await _scanSignal.WaitAsync(cancellationToken);

        // اجمع دفعة الإشعارات قليلاً
        await Task.Delay(TimeSpan.FromMilliseconds(200), cancellationToken);

        Interlocked.Exchange(ref _scanRequested, 0);
        bool full = Interlocked.Exchange(ref _fullRescanRequested, 0) == 1;

        foreach (var bundle in EnumerateReadyBundles(incomingDir, full))
        {
            var claimedPath = Path.Combine(processingDir, bundle.Name);

            if (!TryClaimByRename(bundle.Path, claimedPath))
            {
                continue; // عامل آخر سبق إلى الاكتساب
            }

            var manifest = ReadManifest(Path.Combine(claimedPath, "manifest.json"));

            if (AlreadyProcessed(manifest.IdempotencyKey))
            {
                MoveToArchive(claimedPath, archiveDir);
                continue;
            }

            ProcessBundle(claimedPath);
            RecordProcessed(manifest.IdempotencyKey);
            MoveToArchive(claimedPath, archiveDir);
        }

        if (Volatile.Read(ref _scanRequested) == 1)
        {
            _scanSignal.Release(); // لا تُسقط إشعاراً وصل أثناء المسح
        }
    }
}

المهمّ في هذا المثال ليس تفاصيل API بل التدفّق.

  • طيّ الإشعارات إلى scan request
  • إيجاد ready بالمسح
  • أخذ claim
  • التحقّق من idempotency
  • المعالجة والتسجيل ثمّ النقل إلى archive
تدفّق المعالجة في الاتجاه الصحيحيبيّن التسلسل الذي تمثّله الشيفرة شبه الكاذبة: طيّ الإشعار إلى scan request، وإيجاد المرشّح ready بالمسح، وأخذ claim، والتحقّق من idempotency، ثمّ المعالجة والتسجيل والنقل إلى archive.طيّ الإشعار إلى scan requestإيجاد ready بالمسحأخذ claimالتحقّق من idempotencyالمعالجة والتسجيل ثمّ النقل إلى archive

الشكل 14: هذا التدفّق هو الجسم لا تفاصيل API. الأحداث مجرّد trigger.

أحداث FileSystemWatcher هنا ليست سوى trigger.

لاحظ أنّ EnumerateReadyBundles / TryClaimByRename / ReadManifest / AlreadyProcessed أسماء دوال وُضعت في هذا المقال لإظهار التدفّق، وليست API قياسيّة في .NET. الشكل الذي يُبنى ويُشغَّل فعليّاً (مكتبة، وعرض console على دليل مؤقّت، واختبارات وحدة للتحقّق من الأحداث) موجود في مجموعة العيّنات المذكورة في المقدّمة.

filesystemwatcher-safe-basics - komurasoft-blog-samples (GitHub)

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

  • عامل استقبال واحد / الجانب المرسل قابل للإصلاح لديك أوّلاً temp -> close -> rename ومسح البدء. هذا وحده يثبت كثيراً.

  • وجود أكثر من عامل استقبال يُفضَّل إضافة claim rename من incoming إلى processing فوق ما سبق.

  • إشعارات كثيرة بتردّد عالٍ تضييق Filter / NotifyFilter / IncludeSubdirectories، وتصغير معالج الحدث إلى الحدّ الأدنى. ضبط InternalBufferSize بعد ذلك.

  • ضرر من overflow / الفقد غير مقبول افترض full rescan، وإن بقي الأمر قاسياً فلا تراهن على FileSystemWatcher وحده. على Windows يمكن أن يكون USN change journal خياراً.

  • لا يمكن التحكّم بطريقة كتابة نظام الطرف الآخر التفكير أوّلاً في التفاوض على بروتوكول النشر أثبت من سدّ شرط الاكتمال بالتخمين. إن تعذّر ذلك، تُخفَّض ضمانة المستوى ويُمال التصميم إلى الاستقبال بـ idempotent.

البندان الأخيران قرار انسحاب مهمّ نسبيّاً. FileSystemWatcher مريح، لكنّه ليس كاشف حقيقة كليّ القدرة.

ما الذي يختلف في USN change journal

USN change journal هو سجلّ تغيّرات تحتفظ به NTFS على مستوى المجلّد. إشعارات الدليل مثل FileSystemWatcher لا تُستقبَل إن لم يكن التطبيق يعمل في لحظة التغيّر، أمّا change journal فيبقى السجلّ على المجلّد، لذا يمكن لاحقاً إعادة قراءة التغيّرات أثناء توقّف التطبيق من موضع القراءة السابق (USN). وثائق Microsoft أيضاً تعدّ «ضرورة إبقاء التطبيق يعمل باستمرار» نقطة ضعف لإشعارات الدليل، وتشرح change journal كسبيل لتجاوزها.

في المقابل يزيد العبء.

  FileSystemWatcher USN change journal
وحدة المراقبة الدليل المعيَّن (+ الأدلّة الفرعيّة) المجلّد كلّه. النطاق اللازم تضيّقه بنفسك
أثناء توقّف التطبيق لا يُعرَف. يُسدّ بـ full rescan يمكن إعادة القراءة من السجلّ
الفقد يحدث بـ overflow المخزن الداخليّ السجلّات القديمة تُمحى إذا تجاوزت حدّ المجلّة
المطلوب API في .NET فقط مقبض مجلّد واستدعاءات FSCTL_*. عمليّات الإدارة مثل إنشاء المجلّة وحذفها تحتاج امتيازات مدير

أي أنّه خيار عندما يدخل في المتطلّبات «لا يمكن التشغيل المستمرّ» أو «نريد التقاط تغيّرات فترة التوقّف أيضاً». إن لم يكن ذلك لازماً، فـ FileSystemWatcher + full rescan أبسط في التنفيذ.

الفرق بين FileSystemWatcher وUSN change journalيبيّن أنّ FileSystemWatcher لا يعرف تغيّرات فترة التوقّف فيسدّها بـ full rescan، بينما USN change journal يبقي السجلّ على المجلّد فيعيد قراءة تغيّرات فترة التوقّف من موضع USN السابق.FileSystemWatcherتغيّرات فترة التوقّف لا تُعرَفالسدّ بـ full rescanUSN change journalالسجلّ يبقى على المجلّدإعادة القراءة من USN السابق

الشكل 15: إذا ظهر متطلّب عدم إمكان التشغيل المستمرّ أو التقاط تغيّرات فترة التوقّف، يصير change journal خياراً.

7. الخلاصة

FileSystemWatcher لا يغني عن إشعار الاكتمال. الحقيقة ليست في تسلسل الأحداث، بل في الحالة الظاهرة الآن على القرص. الاكتمال يُصرَّح به عبر temp -> close -> rename / replace أو done / manifest، والملكيّة تُقرَّر بأخذ claim ذرّيّاً. جسم التصميم هنا.

المعالجة فوراً عند Created، والثقة بعدد Changed وترتيبه، واعتبار التوقّف عن Changed اكتمالاً، والاطمئنان بـ InternalBufferSize وحده، وعدم التعافي رغم رؤية Error — كلّها تصميمات يُراد تجنّبها. بدلاً من ذلك تُطوى الإشعارات إلى طلب إعادة مسح، ويُدخَل full rescan عند البدء وoverflow وإعادة الاتّصال، وتُؤخَذ الملكيّة بـ claim rename، ويُستوعَب التكرار وإعادة المسح بـ idempotency.

أي أنّ الحيلة في FileSystemWatcher ألا تُساوَى «استقبال الحدث» بـ «جواز المعالجة». فصل هذين وحدَه يقلّل كثيراً نوع المراقبة الذي ينكسر أحياناً فقط.

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

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

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

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

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

هل يجوز قراءة الملفّ عند حدث Created في FileSystemWatcher؟
لا. Created يعني فقط أنّ الاسم ظهر، ولا يضمن أنّ القراءة صارت مباحة. في النسخ أو النقل قد يُطلَق Created في لحظة إنشاء الملفّ، ثم يتبعه Changed مرّة أو أكثر. الاكتمال يصرّح به الجانب المرسل عبر temp -> close -> rename/replace أو done/manifest. الجانب المستقبل ينظر أساساً إلى الاسم النهائيّ أو إلى done فقط.
هل يمكن أن يفوت FileSystemWatcher إشعارات؟
نعم. إذا فاض المخزن الداخليّ (الافتراضيّ 8192 بايت، لا ينخفض عن 4096، والحدّ الأعلى 64KB) تُفقَد الإشعارات الفرديّة ويُطلَق حدث Error. عند overflow تصبح سلامة تسلسل الأحداث نفسها موضع شكّ، لذا من الآمن إجراء full rescan للدليل ومراجعة الكلّ. ينبغي إدخال full rescan عند البدء، وعند استقبال Error، وبعد إعادة إنشاء watcher مباشرة، وكذلك كتأمين دوريّ.
لماذا يصل حدث Changed عدّة مرّات؟
حتّى العمليات العاديّة مثل النقل أو الحفظ قد تظهر منقسمة إلى عدّة أحداث، ويُلتقَط فوق ذلك ما تلمسه برامج مكافحة الفيروسات أو المفهرِس. التصميم الذي يثق بعدد الأحداث أو ترتيبها خطر. تُطوى الإشعارات إلى إشارة واحدة من نوع «طلب إعادة مسح»، وتُجمَع المسح في عامل واحد، وعند الاندفاع تُجمَّع نحو 100 إلى 300ms ثم تُمسَح مرّة واحدة. هذا أكثر استقراراً.
هل زيادة InternalBufferSize تحلّ الفقد؟
لا. حتّى لو رُفع إلى 64KB، إذا تجاوز اندفاع الإشعارات ذلك الحدّ فسيُفقَد الإشعار، ومسألة هل الإشعار إشعار اكتمال أم لا لا تُحَلّ أصلاً. المخزن يستخدم non-paged memory، لذا الزيادة ليست رخيصة. الترتيب: أوّلاً تضييق الهدف بـ Filter/NotifyFilter، ومراجعة IncludeSubdirectories، وتخفيف معالج الحدث، وإدخال full rescan وidempotency.

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

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

غو كومورا

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

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

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