מדריך מעשי ל-FileSystemWatcher — התמודדות עם פספוסים וכפילויות
· עודכן בתאריך: · Go Komura · FileSystemWatcher, C#, .NET, פיתוח Windows, העברת קבצים, תכנון
FileSystemWatcher הוא ה-API שנחשב לראשון מועמד כשרוצים לנטר שינויי קבצים ב-.NET על Windows. אפשר לקבל כאירועים יצירה, שינוי, מחיקה ושינוי שם של קבצים ותיקיות, וזה נוח, אבל אם משתמשים ב-Created וב-Changed בהנחה שהם הודעת סיום, בהחלט קורות תקלות שגרתיות של פספוסים, התראות כפולות, וקריאה שגויה של קובץ באמצע.
במאמר הזה נסדר את אופן השימוש ואת נקודות התשומת לב של FileSystemWatcher, בהנחה בעיקר של העברת קבצים ב-.NET על Windows. יחד עם זה, גם רעיון בקרת ההרשאות הבלעדיות שמונח בבסיס אפשר לעיין בו ב-ידע בסיסי על בקרת הרשאות בלעדיות בהעברת קבצים — נעילת קבצים ושיטות עבודה מומלצות ל-claim אטומי.
בפועל, יש מקרים ש-Created נשלח קודם באמצע העתקת קובץ, וגם Changed לא בהכרח מגיע פעם אחת בלבד. אם השינויים מתרכזים בזמן קצר, המאגר הפנימי גולש ומפספסים שינויים בודדים.
לכן, עמוד השדרה של התכנון הוא זה.
- ההתראה היא רק טריגר
- האמת היא סריקה חוזרת של הספרייה
- הבעלות נקבעת על ידי claim אטומי
- בסוף קולטים עם אידמפוטנטיות
בגוף המאמר, נעבור לפי הסדר על המכשולים בשילוב FileSystemWatcher להעברת קבצים לפי הגישה הזו.
נציין שהקוד שמופיע במאמר הזה מפורסם ב-GitHub כערכת דוגמה מלאה שאפשר לבנות ולהריץ (ספרייה, הדגמת קונסולה שרצה על תיקייה זמנית, ובדיקות יחידה שיוצרות ומשנות קבצים בפועל כדי לאמת אירועים).
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) | תכונה שבה עיבוד של אותו יעד פעמיים או יותר נותן תוצאה זהה לעיבוד פעם אחת. מכיוון שמניחים מראש התראות כפולות וסריקה חוזרת, בסוף זה מה שקולט את זה (4.5) |
| manifest | קובץ קטן שמתאר את התוכן, שמונח יחד עם גוף הנתונים. אם שמים בו מספר רשומות, hash, IdempotencyKey וכדומה, צד הקבלה יכול להחליט “האם זה כבר עובד” |
| bundle | יחידה שמאגדת יחד העברה אחת. אם שמים גוף + manifest + קבצי עזר בתיקייה אחת, אפשר לתפוס claim לתיקייה כולה ב-rename אחד (4.3) |
| full rescan | לא להסתמך על אירועים, אלא למנות מחדש מאפס את תיקיית המעקב, ולבדוק מחדש את מה שמותר לעבד (4.4) |
| overflow | מצב שבו המאגר הפנימי של FileSystemWatcher גולש, ומאבדים התראות בודדות. מתקבלת התראה על כך באירוע Error (2.3) |
| ready | מצב שבו נקבע ש”כבר מותר לקרוא”. לא מנחשים אלא קובעים לפי קיום שם final או done / manifest (4.2) |
תוכן עניינים
- קודם המסקנה (במשפט אחד)
- 1.1. קודם קוד מינימלי שעובד
- תבניות טעות שקורות עם
FileSystemWatcher(תרשים)- 2.1. חושבים ש-
Createdהוא הודעת סיום - 2.2. סומכים על מספר הפעמים והסדר של
Changed - 2.3. מאבדים שינויים בגלישת המאגר הפנימי
- 2.1. חושבים ש-
- אנטי-דפוסים
- 3.1. מעבדים ישירות בתוך מטפל האירוע
- 3.2. מנסים לשחזר את המצב האמיתי מרצף האירועים
- 3.3. מתייחסים לעצירת
Changedכסיום - 3.4. חושבים שהעלאת
InternalBufferSizeפותרת הכול - 3.5. מתעלמים מ-
Errorורק רושמים ביומן
- שיטות עבודה מומלצות
- 4.1. מקפלים את ההתראות ל”בקשת סריקה חוזרת”
- 4.2. תנאי הסיום מסומן במפורש בצד השולח
- 4.3. צד הקבלה תופס claim באופן אטומי
- 4.4. מבצעים full rescan בהפעלה / overflow / חיבור מחדש
- 4.5. מניחים אידמפוטנטיות מראש
- פסאודו-קוד (קטעים)
- 5.1. תבנית כשל טיפוסית
- 5.2. דוגמה לכיוון נכון (בגדול, ככה)
- חלוקה גסה בין השימושים
- סיכום
- מקורות
מפת הידע של המאמר
המאמר הזה מציע לא להתייחס לאירועי Created/Changed של FileSystemWatcher כהודעת סיום, להימנע מאובדן התראות עקב overflow של המאגר הפנימי ומתכנון ששוחזר מרצף אירועים, ולקפל את כל ההתראות לסוג אחד של בקשת סריקה חוזרת שבודקת את המצב בפועל עם full rescan. הסיום מסומן במפורש על ידי הצד השולח עם temp->rename או done/manifest, וצד הקבלה תופס claim אטומי למועמדים במצב ready שנמצאו בסריקה החוזרת, ובהנחה שהולכים לבדוק כמה פעמים, קולט זאת עם אידמפוטנטיות. כשלא ניתן להריץ כל הזמן, או כשפספוסים אסורים, ממקם את יומן USN change journal כאפשרות נוספת.
flowchart LR
accTitle: מפת הידע של המדריך המעשי ל-FileSystemWatcher
accDescr: תרשים המראה שההתראות של FileSystemWatcher הן רק סימן לשינוי ולא הודעת סיום, שההתראות מקופלות לבקשת סריקה חוזרת ומשולבות עם full rescan ו-claim, ואת הקשר בין אובדן התראות עקב overflow של המאגר הפנימי לבין החלופה של יומן USN.
filesystemwatcher["FileSystemWatcher"]
full_rescan["full rescan (סריקה חוזרת של כל הספרייה)"]
buffer_overflow_event_loss["אובדן התראות עקב גלישת החוצץ הפנימי"]
periodic_directory_listing["סריקת ספרייה מחזורית"]
change_notification_loss["אובדן התראות שינוי"]
internal_buffer_size_tuning["כוונון InternalBufferSize"]
error_event_ignored_antipattern["אנטי-דפוס של רישום אירוע Error ביומן והתעלמות ממנו"]
created_event_misinterpreted_as_complete["פירוש שגוי של Created כהודעת סיום"]
partial_write_read["תקלה של קריאת קובץ שכתיבתו טרם הושלמה"]
sender_side_completion_signaling["סימון מפורש של תנאי הסיום בצד השולח"]
temp_then_rename_publish["פרסום בסדר temp -> close -> rename/replace"]
done_manifest_file["קובץ done/manifest"]
scan_request_coalescing["קיפול ההתראות לכדי בקשת סריקה חוזרת"]
changed_event_order_assumption["אנטי-דפוס של הסתמכות על מספר אירועי Changed וסדרם"]
event_log_state_reconstruction_antipattern["אנטי-דפוס של שחזור מצב מתוך רצף אירועים"]
duplicate_processing["עיבוד כפול (חיוב כפול, שליחה כפולה, אובדן עדכון)"]
atomic_claim["תפיסה אטומית (claim)"]
bundle["bundle (ספריית יחידת השילוב)"]
idempotent_processing["עיבוד שמניח אידמפוטנטיות"]
watcher_downtime_gap["שינויים שהוחמצו בזמן שה-watcher היה מושבת"]
usn_journal["יומן USN (יומן השינויים)"]
ntfs["NTFS"]
admin_rights["הרשאות מנהל"]
filesystemwatcher -->|"עלול לגרום ל"| buffer_overflow_event_loss
full_rescan -.->|"משתמש ב"| periodic_directory_listing
full_rescan -->|"מענה מומלץ ל"| change_notification_loss
full_rescan -->|"מענה מומלץ ל"| buffer_overflow_event_loss
internal_buffer_size_tuning -->|"שימוש לא מומלץ ל"| buffer_overflow_event_loss
error_event_ignored_antipattern -.->|"עלול לגרום ל"| change_notification_loss
filesystemwatcher -.->|"עלול לגרום ל"| created_event_misinterpreted_as_complete
created_event_misinterpreted_as_complete -->|"עלול לגרום ל"| partial_write_read
sender_side_completion_signaling -->|"מענה מומלץ ל"| created_event_misinterpreted_as_complete
sender_side_completion_signaling -->|"משתמש ב"| temp_then_rename_publish
sender_side_completion_signaling -->|"משתמש ב"| done_manifest_file
scan_request_coalescing -->|"מענה מומלץ ל"| changed_event_order_assumption
scan_request_coalescing -->|"מענה מומלץ ל"| event_log_state_reconstruction_antipattern
full_rescan -->|"מענה מומלץ ל"| event_log_state_reconstruction_antipattern
changed_event_order_assumption -->|"עלול לגרום ל"| duplicate_processing
atomic_claim -->|"מונע"| duplicate_processing
bundle -->|"משתמש ב"| atomic_claim
bundle -->|"משתמש ב"| done_manifest_file
idempotent_processing -->|"מענה מומלץ ל"| duplicate_processing
full_rescan -->|"צריך לקדום ל"| atomic_claim
scan_request_coalescing -->|"צריך לקדום ל"| full_rescan
filesystemwatcher -->|"עלול לגרום ל"| watcher_downtime_gap
full_rescan -->|"מצמצם"| watcher_downtime_gap
usn_journal -->|"מענה מומלץ ל"| watcher_downtime_gap
usn_journal -->|"מחייב"| ntfs
usn_journal -.->|"מחייב"| admin_rights
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 26, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
1. קודם המסקנה (במשפט אחד)
- ההתראות של
FileSystemWatcherהן לא הודעת סיום, אלא סימן לתחילת שינוי -
Created/Changed/Renamedיכולים להגיע כפולים, בסדר שונה מהצפוי, ולהיעלם ב-overflow - יציב יותר לא לבצע עיבוד כבד במטפל האירוע, אלא רק לצבור בקשות סריקה חוזרת
- העיקרון הוא לקבוע סיום במפורש עם
temp -> close -> rename / replaceאוdone/ manifest - אם יש כמה עובדים, צריך לתפוס claim באופן אטומי לפני הקריאה
- כוונון
InternalBufferSizeהוא רק אמצעי עזר. בסוף מה שעוזר הוא full rescan ואידמפוטנטיות
במילים אחרות, לא מתייחסים ל-FileSystemWatcher כ”זרם היסטוריה אמיתי”.
יציב יותר להשאיר את ההתראה רק בתור אות של “כדאי ללכת לבדוק בקרוב”.
flowchart TB
accTitle: עמוד השדרה של התכנון במאמר הזה
accDescr: תרשים המראה שההתראה נשארת רק כטריגר, האמת נבדקת בסריקה חוזרת של הספרייה, הבעלות נתפסת ב-claim אטומי, ובסוף אידמפוטנטיות קולטת כפילויות.
notif["ההתראה היא רק טריגר"] --> rescan["האמת נבדקת בסריקה חוזרת של הספרייה"]
rescan --> claim["הבעלות נתפסת ב-claim אטומי"]
claim --> idem["בסוף אידמפוטנטיות קולטת"]
איור 1: עמוד השדרה של התכנון. לא מתייחסים לאירוע כהיסטוריה אמיתית, אלא משאירים אותו רק כ”כדאי ללכת לבדוק בקרוב”.
1.1. קודם קוד מינימלי שעובד
עבור מי שעדיין לא נגע ב-FileSystemWatcher, נציג צורה מינימלית עם מסלול תקין בלבד. הפרקים שאחרי זה הם הסיפור של המלכודות שמתחילות מ”10 השורות האלה עובדות”.
// אפליקציית קונסולה 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לא מגיע אף אירוע. רק רישום handler לא מפעיל דבר - אורך חיי ה-
watcherהוא אורך חיי האפליקציה. אם היקף המשתנה המקומי נגמר וה-watcher מושמד, ההתראות נעצרות שם. אם רוצים שהוא יישאר קבוע, שומרים אותו במקום שממשיך לחיות, כמו שדה - ברירת המחדל של
NotifyFilterהיא השילובLastWrite | FileName | DirectoryName(ראו FileSystemWatcher.NotifyFilter Property במקורות בפרק 8). כדאי לציין במפורש מה תופסים, כדי לא להתלבט בקריאה חוזרת בהמשך
מה שחשוב הוא שהקוד הזה מוודא רק ש”ההתראה מגיעה”. הוא לא אומר בכלל אם מותר לקרוא את הקובץ בזמן Created, ולא אם ההתראות מפוספסות. מכאן זה מתחיל את הנושא האמיתי.
2. תבניות טעות נפוצות באופן השימוש ב-FileSystemWatcher (תרשים)
2.1. חושבים ש-Created הוא הודעת סיום
זו המוקש הברור ביותר.
בהעתקה או בהעברה, Created יכול להישלח ברגע שהקובץ נוצר, ואחריו יכול להמשיך Changed פעם אחת או יותר.
sequenceDiagram
accTitle: קריאת קובץ פגום כשקוראים ל-Created כהודעת סיום
accDescr: תרשים רצף המראה שהצד השולח יוצר את orders.csv ומקבל Created מיד, אך כשהצד המקבל פותח וקורא באותו רגע ההעתקה עדיין נמשכת, ומגיעים עוד אירועי Changed בזמן שהקובץ נקרא בחוסר שורות או פגום.
participant Sender as הצד השולח
participant Share as watched dir
participant W as FileSystemWatcher
participant Receiver as הצד המקבל
Sender->>Share: יוצר את orders.csv
Share-->>W: Created
W-->>Receiver: OnCreated
Receiver->>Share: פותח וקורא את orders.csv
Note over Receiver: עדיין באמצע ההעתקה
Sender->>Share: כותב את השאר
Share-->>W: Changed
Share-->>W: Changed
Note over Receiver: חוסר שורות / JSON פגום / ZIP פגום
איור 2: גם באמצע ההעתקה נשלח Created. אם קוראים ברגע שהוא מתקבל, נתקלים בנתונים פגומים.
Created מייצג ש”השם נראה”, אבל לא מבטיח ש”כבר מותר לקרוא”.
אם משווים ביניהם, נופלים במסלול אחר לאותה תקרית של 2.1 מהמאמר הקודם.
2.2. סומכים על מספר הפעמים והסדר של Changed
Changed לא בהכרח מגיע פעם אחת בלבד.
גם בפעולות רגילות כמו העברה או שמירה, אפשר לראות אותו מחולק לכמה אירועים. בנוסף, נתפסים גם מגעים של תוכנת אנטי-וירוס או אינדקסר.
sequenceDiagram
accTitle: ריבוי אירועי Changed בשמירה רגילה עם מגעים חיצוניים
accDescr: תרשים רצף המראה שגם שמירה רגילה של report.xlsx מייצרת Created ו-Changed, שאחרי rename מקובץ זמני מתקבלים גם Renamed וגם Changed, ושסריקת אנטי-וירוס או אינדקסר מוסיפה עוד Changed - לא בהכרח פעם אחת ולא בהכרח באותו סדר.
participant App as אפליקציה ששומרת
participant Dir as watched dir
participant AV as אנטי-וירוס / אינדקסר
participant W as FileSystemWatcher
App->>Dir: מתחיל לשמור את report.xlsx
Dir-->>W: Created
Dir-->>W: Changed
App->>Dir: מבצע rename מקובץ זמני
Dir-->>W: Renamed
Dir-->>W: Changed
AV->>Dir: סריקה / קריאת תכונות
Dir-->>W: Changed
Note over W: לא בהכרח פעם אחת, ולא בהכרח בסדר הזה
איור 3: גם בשמירה רגילה האירוע מתחלק לכמה, ומעורבים גם מגעים מתהליכים חיצוניים. גם המספר וגם הסדר לא אמינים.
הציפייה ש”אם Changed הגיע פעם אחת זה סיום” או “אחרי Renamed כבר לא נוגעים בו” מסוכנת למדי.
הערה:
-
Changedעלול להישלח גם בגלל rename של קובץ -
RenamedEventArgs.Nameיכול להיותnullאם מערכת ההפעלה לא הצליחה להתאים בין old/new - גם קובץ מוסתר (hidden) לא מתעלמים ממנו. “זה שם temp מוסתר אז לא ייראה” לא נכון
- אם מבצעים rename לתיקייה המנוטרת עצמה, השינוי הזה לא מתקבל כהתראה
2.3. מאבדים שינויים בגלישת המאגר הפנימי
ל-FileSystemWatcher יש מאגר פנימי.
אם השינויים מתרכזים בזמן קצר, הוא גולש ומאבדים התראות בודדות.
flowchart LR
accTitle: מ-overflow של המאגר הפנימי ועד full rescan
accDescr: תרשים המראה שהרבה שינויים בזמן קצר מצטברים במאגר הפנימי, ואם העיבוד לא מדביק מתרחש overflow שמייצר אירוע Error, ובגלל שאי אפשר לסמוך על שלמות ההיסטוריה הבודדת מבצעים full rescan לספרייה.
A["הרבה שינויים בזמן קצר"] --> B["ההתראות מצטברות במאגר הפנימי"]
B --> C{"העיבוד מדביק?"}
C -- "כן" --> D["מעבדים כל אירוע לפי הסדר"]
C -- "לא" --> E["overflow"]
E --> F["אירוע Error"]
F --> G["לא סומכים על שלמות ההיסטוריה הבודדת"]
G --> H["מבצעים 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 כבד או עדכון DB, חונקים את עצמכם בזמן פרץ התראות.
flowchart TB
accTitle: נקודת ההכרעה לגבי כובד מטפל האירוע
accDescr: תרשים המראה שמטפל האירוע כדאי שירים בקשת סריקה חוזרת ויחזור מיד, ואילו I/O כבד או עדכון DB בתוך המטפל יוצרים סיכון של קריאת תוכן לא שלם וקושי להדביק בזמן פרץ התראות.
ev["מטפל האירוע"] --> light["מרים בקשת סריקה חוזרת וחוזר מיד"]
heavy["I/O כבד או עדכון DB בתוך המטפל"] -.-> raw["סיכון של קריאת תוכן לא שלם"]
heavy -.-> choke["קושי להדביק בזמן פרץ התראות"]
איור 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;
}
חזק יותר לבדוק בכל פעם מחדש את המצב בפועל על הדיסק, מאשר להתעקש על הכיוון הזה. מה שחשוב בהעברת קבצים הוא למצוא נכון את היעד שמותר לעבד ברגע הזה בדיוק, לא לשחזר בנאמנות את היסטוריית האירועים.
flowchart TB
accTitle: השוואה בין שחזור מהאירועים לבדיקת המצב בפועל
accDescr: תרשים המראה ששחזור מצב מרצף אירועים נשבר בכפילות, פיצול, overflow והפרעות חיצוניות, בעוד שבדיקה חוזרת של המצב בפועל על הדיסק בכל פעם מוצאת נכון יותר את היעד שמותר לעבד.
ev2["שחזור מצב מרצף האירועים"] -.-> broke["נשבר בכפילות, פיצול ו-overflow"]
disk["בודקים כל פעם את המצב בפועל על הדיסק"] --> goal["מוצאים נכון את היעד שמותר לעבד"]
איור 6: המטרה היא לא לשחזר את היסטוריית האירועים, אלא למצוא את היעד שמותר לעבד עכשיו.
3.3. מתייחסים לעצירת Changed כסיום
זה תכנון מאותה משפחה כמו “אם גודל הקובץ הפסיק להשתנות זה סיום” מהמאמר הקודם. נראה נוח, אבל קובעים סיום על סמך ניחוש.
if (lastChangedAt + TimeSpan.FromSeconds(10) < DateTime.UtcNow)
{
return Ready;
}
זה יוצר בעיה, לדוגמה, במקרים כאלה.
- העתקת קובץ גדול משתהה זמנית באמצע
- הצד השולח שומר בכמה שלבים
- ההתראה נראית מאוחרת בתיקייה משותפת ברשת
- תהליך חיצוני כותב מחדש תכונות או חותמות זמן בהמשך
יציב יותר לקבוע סיום במפורש, לא בניחוש.
flowchart TB
accTitle: הסיכון שבניחוש סיום לפי שקט
accDescr: תרשים המראה שהנחה שכשה-Changed נעצר לזמן מה זה סיום, מתפספסת בהשהיה זמנית של ההעתקה, שמירה רב-שלבית והשהיית ההתראה, ולכן יציב יותר שהסיום יסומן במפורש בצד השולח.
guess["הנחת סיום כשה-Changed נעצר"] -.-> c1["מתפספסת בהשהיית העתקה"]
guess -.-> c2["מתפספסת בשמירה רב-שלבית ובעיכוב התראה"]
fix["הסיום מסומן במפורש בצד השולח"] --> stable["יציב, לא תלוי בניחוש"]
איור 7: שקט לא מהווה הוכחה לסיום. קובעים סיום במפורש, לא בניחוש.
3.4. חושבים שהעלאת InternalBufferSize פותרת הכול
כוונון InternalBufferSize חשוב, אבל זה לא גוף התכנון.
- ברירת המחדל היא
8192בייט - אי אפשר להקטין מתחת ל-
4096בייט, ולא ניתן לחרוג מ-64 KB - המאגר משתמש בזיכרון non-paged, ולכן ככל שמגדילים, זה לא עניין קל דעת
כלומר, גם אם מעלים עד 64 KB, ברגע שפרץ ההתראות עולה על זה - זה נגמר.
ובנוסף, זה לא פותר ולו במעט את שאלת “האם זו הודעת סיום”.
לפני שמגדילים את המאגר, יש דברים שכדאי לטפל בהם קודם.
- לצמצם את מטרת המעקב עם
Filter/Filters - לצמצם את
NotifyFilterלמינימום הנדרש - לא להפוך
IncludeSubdirectoriesל-trueבלי סיבה - להקל על מטפל האירוע
- להכניס full rescan ואידמפוטנטיות
flowchart TB
accTitle: מה עושים קודם, לפני הגדלת המאגר
accDescr: תרשים המראה שגם אם מעלים את InternalBufferSize עד 64KB, אם הפרץ עולה על זה עדיין יש פספוסים, ולכן כדאי לצמצם קודם עם Filter ו-NotifyFilter, להקל על המטפל, ולהכניס full rescan ואידמפוטנטיות.
first["דברים לטפל בהם קודם"] --> f1["צמצום עם Filter ו-NotifyFilter"]
first --> f2["הקלה על מטפל האירוע"]
first --> f3["full rescan ואידמפוטנטיות"]
buf["כוונון InternalBufferSize"] -.-> aux["רק אמצעי עזר אחרון"]
איור 8: הגדלת המאגר היא לא גוף התכנון. הצמצום ומנגנון ההתאוששות קודמים.
3.5. מתעלמים מ-Error ורק רושמים ביומן
Error הוא לא סוג של התראה מסוג “מדי פעם מופיע אבל לא צריך לשים לב”.
כאן מופיעים buffer overflow או מצב שבו נכשל המשך המעקב.
watcher.Error += (_, e) =>
{
_logger.LogError(e.GetException(), "watcher error");
// אם זה נגמר כאן, שמים לב לפספוס אבל לא מתאוששים
};
לפחות, כדאי לעשות עד כאן.
- לבקש full rescan
- אם המשך המעקב מוטל בספק, לשקול גם יצירה מחדש של ה-watcher
- להכין אפשרות לעיבוד חוזר אידמפוטנטי בהנחה שיש פספוסים
4. שיטות עבודה מומלצות
4.1. מקפלים את ההתראות ל”בקשת סריקה חוזרת”
אם מחברים ישירות Created / Changed / Deleted / Renamed / Error כל אחד לעיבוד עסקי נפרד, הבהירות נפגעת. קודם מקפלים את כולם לסוג אחד של אות - “לך לבדוק”.
flowchart LR
accTitle: קיפול כל האירועים לבקשת סריקה חוזרת אחת
accDescr: תרשים המראה שאירועי Created/Changed/Deleted/Renamed, אירועי Error/overflow, וגם ה-startup, כולם מקופלים לאות scan request אחד, שמפעיל סריקה חוזרת של הספרייה, מונה מועמדים במצב ready, ומנסה claim.
A["Created / Changed / Deleted / Renamed"] --> Q["scan request"]
B["Error / overflow"] --> Q
C["startup"] --> Q
Q --> D["סריקה חוזרת של הספרייה"]
D --> E["מונים מועמדים במצב ready"]
E --> F["מנסים claim"]
איור 9: כל התראה וגם ה-startup מקופלים לסוג אחד של scan request, הסריקה החוזרת מחפשת מועמדים במצב ready, ומנסים claim.
נקודות מרכזיות למימוש:
- במטפל האירוע רק קובעים
dirty = trueומרימים signal - הסריקה מרוכזת ב-worker אחד
- בזמן פרץ, מרכזים כ-100 עד 300ms ואז מבצעים סריקה אחת
- אם מגיעה התראה נוספת בזמן הסריקה, מבצעים עוד סריקה אחת אחרי שהיא מסתיימת
הערך של 100 עד 300ms בסעיף השלישי אינו מספר עם בסיס בתקן או בתיעוד רשמי, אלא ערך התחלתי מניסיון תפעולי של הכותב. בפועל, בטוח יותר להחליט אחרי שמודדים את שני אלה.
| מה בודקים | איך מחליטים |
|---|---|
| זמן הסריקה האחת | אם ההמתנה קצרה מזה, מצטברות רק בקשות סריקה נוספות לפני שהסריקה מסתיימת. אורך זמן הסריקה עצמו הוא קירוב טוב לגבול התחתון |
| השהיית הזיהוי המותרת | ההמתנה הופכת ישירות לעיכוב בזיהוי. אם יש דרישה כמו “לעבד תוך n שניות מהנחת הקובץ”, שמים את התקרה בטווח שנשאר בתוך חלק מזה |
לדוגמה, אם סריקה אחת מסתיימת ב-50ms והזיהוי מותר תוך שנייה, הטווח 100 עד 300ms מתאים בדיוק. לעומת זאת, אם יש הרבה קבצים וסריקה אחת אורכת כמה שניות, יעיל יותר לבדוק מחדש את מבנה הסריקה (צמצום היעד, הסתכלות רק על done, פיצול תיקיות משנה) לפני שמאריכים את ההמתנה.
בצורה כזו, בין אם ההתראה מגיעה 5 פעמים או 50 פעמים, מה שעושים בסוף מתאחד ל”בודקים את המצב בפועל ומחפשים מה במצב ready”.
4.2. תנאי הסיום מסומן במפורש בצד השולח
אם אתם שולטים גם בצד השולח, יעיל יותר לתקן את פרוטוקול הפרסום, במקום להתאמץ לקבוע סיום בצד FileSystemWatcher.
הדרך הסטנדרטית היא כמובן זו.
- כותבים את כל התוכן לשם
temp - מבצעים
close - מבצעים
rename / replaceבאותה מערכת קבצים - אם צריך, מניחים
done/ manifest בסוף
flowchart TD
accTitle: פרוטוקול הפרסום עם קובץ temp ו-rename
accDescr: תרשים המראה שהצד השולח כותב את כל התוכן ל-data.tmp, מבצע flush וסגירה, מבצע rename או replace לשם הסופי data.csv, ומניח data.done או manifest.json, כך שהצד המקבל מסתכל רק על שם ה-final או ה-done.
A["כותבים את כל התוכן ל-data.tmp"] --> B["flush / close"]
B --> C["rename / replace ל-data.csv"]
C --> D["מניחים data.done / manifest.json"]
D --> E["צד הקבלה מסתכל רק על שם final או done"]
איור 10: הצד השולח כותב את כל התוכן ל-temp, סוגר, מפרסם עם rename, ואם צריך מניח done / manifest בסוף.
זהה למאמר הקודם, אבל זה באמת יעיל כאן.
טבעי יותר לתפוס את FileSystemWatcher לא ככלי שממציא סיום, אלא ככלי שמוצא מוקדם סיום שסומן במפורש.
4.3. צד הקבלה תופס claim באופן אטומי
גם אם נמצא מועמד ready בסריקה החוזרת, אם הולכים לקרוא אותו כמו שהוא, כמה עובדים יכולים לתפוס אותו בו-זמנית. לכן, תופסים claim באופן אטומי לפני העיבוד.
sequenceDiagram
accTitle: תפיסת claim אטומי כשכמה עובדים מתחרים על אותו מועמד
accDescr: תרשים רצף המראה שה-scanner מוצא את order-123 ומנסה rename אליו גם מ-worker1 וגם מ-worker2, ורק מי מהשניים שהצליח ראשון ב-rename מחזיק בבעלות על העיבוד.
participant Scan as scanner
participant IN as incoming
participant P1 as processing/worker1
participant P2 as processing/worker2
Scan->>IN: מוצא את order-123
Scan->>P1: מבצע rename ל-order-123
Scan->>P2: מבצע rename ל-order-123
Note over P1,P2: רק מי שהצליח ראשון מחזיק בבעלות
איור 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 הוא רמז לדיפרנציאל, הסריקה החוזרת היא ההתאוששות לשלמות”.
flowchart TB
accTitle: העיתויים להכניס full rescan
accDescr: תרשים המראה שארבעה עיתויים - בזמן ההפעלה, בקבלת Error, מיד אחרי יצירה מחדש של ה-watcher, וכביטוח תקופתי במרווח קבוע - מכניסים בהם full rescan כדי להתאושש לשינויים שהאירועים לא תפסו.
t1["בזמן ההפעלה"] --> fr["full rescan"]
t2["בקבלת Error"] --> fr
t3["מיד אחרי יצירה מחדש של ה-watcher"] --> fr
t4["כביטוח תקופתי"] --> fr
fr --> heal["התאוששות לשלמות"]
איור 12: ה-watcher הוא רמז לדיפרנציאל, ו-full rescan הוא ההתאוששות לשלמות. מכניסים אותו בארבעת העיתויים האלה תמיד.
4.5. מניחים אידמפוטנטיות מראש
כשמשתמשים ב-FileSystemWatcher, הולכים לבדוק את אותו יעד כמה פעמים.
זה לא באג, ויציב יותר לקבל את זה כחלק מהתכנון.
באופן קונקרטי, זה נראה כך.
- מכניסים
IdempotencyKeyל-manifest - אם כבר עובד, לא מריצים מחדש תופעות לוואי
- אפשר להצליב מול archive-ed / מתועד ב-DB / נשלח
- גם אחרי full rescan, זה רק “לראות שוב באופן בטוח את אותו הדבר”
לנסות ליצור exactly-once רק באמצעות האירועים די קשה. מקבלים at-least-once, וסוגרים בסוף עם אידמפוטנטיות - זה חזק יותר בפועל בשטח.
flowchart TB
accTitle: איך קולטים כפילות בהנחה מראש
accDescr: תרשים המראה שהתייחסות לבדיקה חוזרת של אותו יעד כמה פעמים לא כבאג אלא כחלק מהתכנון, הצלבה מול IdempotencyKey ב-manifest, ואי-הרצה חוזרת של תופעות לוואי, גורמת לכך שגם full rescan הופך לבדיקה בטוחה בלבד.
multi["בודקים את אותו יעד כמה פעמים"] --> accept["מקבלים כחלק מהתכנון"]
accept --> key["מצליבים מול IdempotencyKey"]
key --> safe["לא מריצים מחדש תופעות לוואי"]
safe --> strong["גם full rescan הופך לבדיקה בטוחה"]
איור 13: לא יוצרים exactly-once באמצעות האירועים, אלא מקבלים at-least-once וסוגרים עם אידמפוטנטיות.
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
- בודקים אידמפוטנטיות
- מעבדים, מתעדים, ומעבירים ל-archive
flowchart TB
accTitle: הזרימה של הכיוון הנכון בעיבוד
accDescr: תרשים המראה שמקפלים את ההתראות ל-scan request, מוצאים בסריקה מועמד ready, תופסים claim, בודקים אידמפוטנטיות, ואז מעבדים, מתעדים, ומעבירים ל-archive - הזרימה שמייצג הפסאודו-קוד.
n["מקפלים התראות ל-scan request"] --> s["מוצאים ready בסריקה"]
s --> c["תופסים claim"]
c --> i["בודקים אידמפוטנטיות"]
i --> p["מעבדים, מתעדים, ומעבירים ל-archive"]
איור 14: הזרימה הזו היא הגוף, יותר מה-API הפרטני. האירוע כאן הוא רק trigger.
האירועים של FileSystemWatcher, כאן, הם רק trigger.
נציין ש-EnumerateReadyBundles / TryClaimByRename / ReadManifest / AlreadyProcessed וכדומה הן פונקציות ששמנו להן שם כדי להראות את הזרימה במאמר הזה, ולא API סטנדרטי של .NET. הצורה שבאמת בונים ומריצים (ספרייה, הדגמת קונסולה שרצה על תיקייה זמנית, בדיקות יחידה שמאמתות אירועים) נמצאת בערכת הדוגמה המלאה שהוזכרה בפתיחה.
filesystemwatcher-safe-basics - komurasoft-blog-samples (GitHub)
6. חלוקה גסה בין השימושים
-
עובד קבלה יחיד / שולטים גם בצד השולח קודם
temp -> close -> renameוסריקת startup. זה בלבד כבר די יציב. -
יש כמה עובדי קבלה בנוסף לעיל, טוב יותר להוסיף claim rename מ-
incomingל-processing. -
תדירות התראות גבוהה מצמצמים
Filter/NotifyFilter/IncludeSubdirectories, ומקטינים למינימום את מטפל האירוע. כוונוןInternalBufferSizeבא אחר כך. -
קשה עם overflow / פספוסים אסורים מניחים full rescan כברירת מחדל, ואם זה עדיין לא מספיק, לא כדאי להמר על
FileSystemWatcherבלבד. אם מוגבלים ל-Windows, גם USN change journal הוא אפשרות. -
לא שולטים באופן הכתיבה של המערכת השנייה בטוח יותר לחשוב קודם על אפשרות למשא ומתן על פרוטוקול הפרסום, במקום להשלים תנאי סיום בניחוש. אם זה לא אפשרי, מורידים את רמת ההבטחה ומתקרבים לתכנון שמקבל בצורה אידמפוטנטית.
שני הפריטים האחרונים הם החלטת נסיגה חשובה למדי.
FileSystemWatcher נוח, אבל הוא לא גלאי אמת כל-יכול.
מה ההבדל של יומן USN change journal
יומן USN change journal הוא רשומת שינויים שה-NTFS מחזיק ברמת הכרך. התראת תיקייה כמו FileSystemWatcher לא מתקבלת אם האפליקציה לא רצה ברגע שהשינוי קרה, אבל ה-change journal משאיר רשומה בצד הכרך, ולכן גם שינויים שקרו בזמן שהאפליקציה הייתה כבויה אפשר לקרוא מחדש מהמיקום (USN) שנקרא בפעם הקודמת. גם בתיעוד של Microsoft, כחולשה של התראת תיקייה מוזכר ש”צריך להריץ את האפליקציה כל הזמן”, וכפתרון לזה מוסבר ה-change journal.
מצד שני, גם העומס גדל.
FileSystemWatcher |
יומן USN change journal | |
|---|---|---|
| יחידת המעקב | התיקייה שצוינה (+ תיקיות משנה) | הכרך כולו. את הטווח הנדרש מצמצמים בעצמכם |
| בזמן שהאפליקציה הייתה כבויה | לא ידוע. משלימים עם full rescan | אפשר לקרוא מחדש מהרשומה |
| פספוסים | קורים ב-overflow של המאגר הפנימי | כשעוברים את תקרת היומן, נמחקות הרשומות הישנות |
| מה נדרש | רק API של .NET | handle לכרך וקריאה ל-FSCTL_*. לפעולות ניהול כמו יצירה/מחיקה של היומן נדרשות הרשאות מנהל |
כלומר, זו אפשרות למקרים שבהם “אי אפשר להריץ כל הזמן” או “רוצים לתפוס גם שינויים בזמן ההשבתה” נכנסים לדרישות. להפך, אם זה לא נדרש, FileSystemWatcher + full rescan פשוט יותר למימוש.
flowchart TB
accTitle: ההבדל בין FileSystemWatcher ליומן USN change journal
accDescr: תרשים המראה ש-FileSystemWatcher לא יודע על שינויים בזמן שהאפליקציה כבויה וממלא זאת עם full rescan, בעוד יומן USN change journal משאיר רשומה בצד הכרך ואפשר לקרוא מחדש מהמיקום הקודם גם שינויים בזמן ההשבתה.
fsw["FileSystemWatcher"] -.-> gap["לא יודע על שינויים בזמן ההשבתה"]
gap --> fill["ממלא זאת עם full rescan"]
usn["יומן USN change journal"] --> keep["הרשומה נשארת בצד הכרך"]
keep --> resume["אפשר לקרוא מחדש מה-USN הקודם"]
איור 15: כשנכנסות לתמונה דרישות של “אי אפשר להריץ כל הזמן” או “רוצים לתפוס גם שינויים בזמן השבתה”, ה-change journal הופך לאפשרות.
7. סיכום
FileSystemWatcher אינו תחליף להודעת סיום. האמת נמצאת לא ברצף האירועים, אלא במצב שנראה עכשיו על הדיסק. הסיום מסומן במפורש עם temp -> close -> rename / replace או done / manifest, והבעלות נקבעת בתפיסת claim באופן אטומי. גוף התכנון נמצא כאן.
עיבוד מיידי ב-Created, הסתמכות על מספר הפעמים או הסדר של Changed, התייחסות לעצירת Changed כסיום, הסתפקות ב-InternalBufferSize בלבד, ראיית Error בלי להתאושש - כל אלה תכנונים שכדאי להימנע מהם. במקום זאת, מקפלים את ההתראות לבקשת סריקה חוזרת, מכניסים full rescan בהפעלה / overflow / חיבור מחדש, תופסים בעלות עם claim rename, וקולטים כפילות וסריקה חוזרת עם אידמפוטנטיות.
במילים אחרות, ב-FileSystemWatcher הטריק הוא לא להשוות בין “התקבל אירוע” לבין “מותר לעבד”.
רק ההפרדה הזו מקטינה משמעותית עיבוד ניטור מהסוג שנשבר רק מדי פעם.
8. מקורות
- ערכת קוד הדוגמה המלאה של המאמר הזה (ספרייה, הדגמה, בדיקות יחידה) https://github.com/gomurin0428/komurasoft-blog-samples/tree/main/filesystemwatcher-safe-basics
- מאמר קשור: ידע בסיסי על בקרת הרשאות בלעדיות בהעברת קבצים — נעילת קבצים ושיטות עבודה מומלצות ל-claim אטומי
- FileSystemWatcher Class (System.IO)
- System.IO.FileSystemWatcher class - .NET
- FileSystemWatcher.InternalBufferSize Property (System.IO)
- FileSystemWatcher.NotifyFilter Property (System.IO)
- FileSystemWatcher.Error Event (System.IO)
- FileSystemWatcher.Created Event (System.IO)
- FileSystemWatcher.Changed Event (System.IO)
- FileSystemWatcher.Renamed Event (System.IO)
- Change Journals - Win32 apps
- Creating, Modifying, and Deleting a Change Journal - Win32 apps
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
למה להשתמש ב-Generic Host וב-BackgroundService של .NET באפליקציית desktop
בכלי Windows ובאפליקציות שרצות ברקע, מסודר כאן איך להשתמש ב-Generic Host וב-BackgroundService כדי לסדר הפעלה, עיבוד תקופתי, סיום, לוג, הג...
רשימת בדיקה לפני מעבר מ-.NET Framework ל-.NET
רשימת בדיקה מעשית לפני מעבר מ-.NET Framework ל-.NET, שבודקת סוג פרויקט, טכנולוגיות לא נתמכות, תלויות NuGet, סגנון SDK, WPF/WinForms, ...
מה זה .NET Generic Host - התשתית ל-DI, תצורה ולוגים
מסדרים את התפקיד של Generic Host דרך היחסים בין DI, תצורה, לוגים, IHostedService ו-BackgroundService, ומסכמים מנקודת מבט מעשית איפה הוא ב...
מה זה .NET Native AOT — ההבדל מ-JIT ומ-trimming
מהו Native AOT — סקירה לפי ההבדל מ-JIT, ReadyToRun, self-contained, single-file, trimming ו-source generator, וסיכום מעשי של המקרים ש...
שלושה סוגי טיימרים ב-.NET — מתי להשתמש ב-PeriodicTimer/Timer/DispatcherTimer
מסודר כאן ההבדל בין PeriodicTimer / System.Threading.Timer / DispatcherTimer, ואיך לבחור ביניהם לעיבוד async, callback ב-ThreadPool וע...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
פיתוח יישומי Windows
העברת קבצים וכלי ניטור שמשתמשים ב-FileSystemWatcher הם נושא שעולה הרבה בפועל בשטח, גם בתוך פיתוח אפליקציות Windows.
ייעוץ טכני וסקירת תכנון
אם תרצו לסדר כתכנון את ההתמודדות עם פספוסים, סריקה חוזרת וקביעת סיום, זה מתאים היטב לייעוץ טכני וסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מותר לקרוא קובץ באירוע 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 מגיע כמה פעמים?
- כי גם פעולות רגילות כמו העברה או שמירה יכולות להיראות מחולקות לכמה אירועים, ובנוסף נתפסים גם מגעים של תוכנת אנטי-וירוס או אינדקסר. תכנון שסומך על מספר או סדר האירועים מסוכן. כדאי לקפל את ההתראות לסוג אחד של אות - "בקשת סריקה חוזרת" - לרכז את הסריקה לworker אחד, ולייצב על ידי ריכוז זמן של כ-100 עד 300ms לפני סריקה אחת בזמן פרץ התראות.
- האם הגדלת InternalBufferSize פותרת פספוסים?
- לא פותרת. גם אם מעלים עד 64KB, אם פרץ ההתראות עולה על זה, עדיין תיווצר החמצה, וזה לא פותר כלום בשאלה אם זו הודעת סיום. המאגר משתמש בזיכרון non-paged, ולכן הגדלתו אינה עניין קל דעת. הסדר הנכון הוא לצמצם קודם את מטרת המעקב עם Filter/NotifyFilter, לבדוק מחדש את IncludeSubdirectories, להקל על מטפל האירוע, ולהכניס full rescan ואידמפוטנטיות.