. jak .NET

.NET & stuff blog by Maciej "Procent" Aniserowicz

Problem z FileSystemWatcher.Created

9 maja 2008 10:28 w kategorii: pro
Tagi: ,

Klasa System.IO.FileSystemWatcher jest momentami wprost niezastąpiona. Nie będę opisywał tutaj jej cech, ale zajmę się jednym problemem. Zdarzenie Created daje nam znać o tym, że nowy plik pojawił się w obserwowanym katalogu. Co się jednak może stać, gdy beztrosko zaczniemy się owym plikiem zajmować? Prawdopodobne jest, że otrzymamy wyjątek. Powód takiego zachowania jest taki, że zdarzenie Created informauje nas o momencie UTWORZENIA pliku, a nie jego GOTOWOŚCI DO OBRÓBKI. W przypadku większych plików od momentu utworzenia go w katalogu do zakończenia procesu kopiowania jego zawartości może się przesypać sporo piachu w klepsydrze Piaskowego Dziadygi. Dlatego też stworzyłem klasę dziedziczącą z Watchera, która udostępnia zdarzenie AfterCreated - odpalane w momencie zakończenia tworzenia nowego pliku.
Niestety programiści tworzący FileSystemWatcher nie do końca dostosowali się do praktyk związanych z tworzeniem zdarzeń i metody OnCreated, OnRenamed itd nie są oznaczone jako wirtualne. Dlatego też podpinam się do zdarzenia Created. Po jego wystąpieniu w nowym wątku próbuję otworzyć docelowy plik - jeśli się udaje to odpalam swoje zdarzenie AfterCreated. Jeżeli nie - przechwytuję wyjątek, czekam jakiś czas i próbuję znowu. I tak w koło Macieju. Pod uwagę wzięty został równiez scenariusz usunięcia pliku w okresie pomiędzy końcem kopiowania a sprawdzeniem dostępności. W tym przypadku łapię wyjątek FileNotFoundException i kończę wątek. W celu uniknięcia problemów z wątkami i aktualizacją UI wykorzystany został mechanizm oferowany przez klasy AsyncOperation/AsyncOperationManager.
ENJOY:

 1:   public class FileSystemWatcherEx : FileSystemWatcher
2: {
3: public event EventHandler<FileSystemEventArgs> AfterCreated;
4: protected virtual void OnAfterCreated(FileSystemEventArgs e)
5: {
6: if (AfterCreated != null)
7: AfterCreated(this, e);
8: }
9:
10: private const int DEFAULT_CHECK_AVAILABILITY_INTERVAL = 500;
11: private int _checkAvailabilityInteval = DEFAULT_CHECK_AVAILABILITY_INTERVAL;
12: [DefaultValue(DEFAULT_CHECK_AVAILABILITY_INTERVAL)]
13: [Category("Behavior")]
14: [Description("Determines how ofter the file is checked for availability.")]
15: public int CheckAvailabilityInteval
16: {
17: get { return _checkAvailabilityInteval; }
18: set { _checkAvailabilityInteval = value; }
19: }
20:
21: public FileSystemWatcherEx()
22: {
23: base.Created += (sender, e) =>
24: {
25: AsyncOperation operation = AsyncOperationManager.CreateOperation(null);
26:
27: // start a new thread to watch the file
28: ThreadPool.QueueUserWorkItem(delegate
29: {
30: bool canOpen = false;
31: // loop while the file cannot be opened -> is still being used by another process
32: while (canOpen == false)
33: {
34: try
35: {
36: // try to open the file for reading - and close it immidiately if succeeded
37: File.OpenRead(e.FullPath).Close();
38: canOpen = true;
39: }
40: // can occur when a file is removed directly after processing
41: catch (FileNotFoundException)
42: {
43: break;
44: }
45: // occurs when trying to open a directory rather than a file
46: catch (UnauthorizedAccessException)
47: {
48: break;
49: }
50: catch (IOException)
51: {
52: // wait and try again
53: Thread.Sleep(CheckAvailabilityInteval);
54: }
55: }
56: if (canOpen)
57: {
58: operation.Post(delegate
59: {
60: OnAfterCreated(e);
61: }, null);
62: }
63: });
64: };
65: }
66: }

Komentarze

apl

9 maja 2008 13:37

Zdarzenie Create jest wywoływane również wówczas, gdy utworzony został nowy folder - uważaj na to. Ja osobiście próbowałbym rozwiązać problem inaczej, korzystając z pewnego charakterystycznego zachowania FileSystemWatcher - po zdarzeniu Created wywoływane jest przynajmniej raz zdarzenie Changed. Niestety nie da się przewidzieć, ile takich zdarzeń wystąpi podczas tworzenia pliku, można natomiast w metodzie obsługi zdarzenia Created podpiąć się pod zdarzenie Changed i w nim sprawdzać, czy plik da się otworzyć, najlepiej żądając prawa dostępu do pliku na wyłączność (tj. wywołując File.Open(ścieżka, FileMode.Open, FileAccess.Read, FileShare.None)). Gdy żądanie zostanie spełnione, można odpiąć handler i wywołać zdarzenie informujące o dostępności pliku.

Tom

9 maja 2008 13:45

:) No i nie przeczytam drugiego akapitu :)

Procent

9 maja 2008 14:21

@Apl: faktycznie, umknął mi fakt że dla Directory zostanie odpalone to samo. Co do rozwiązania z Changed - wyglądałoby mniej szpanersko niż te wszystkie AsyncOperation, ThreadPool, anonimowych metod i wyrażeń lambda;).

@Tom: co z drugim akapitem? znika czasami? ;)

Procent

9 maja 2008 14:50

@Apl
Po chwili namysłu: do rozwiązania z Created trzeba by było dodać listę aktualnie obserwowanych plików tak, aby zdarzenie AfterCreated nie zostało wywołane dla już istniejącego pliku zmodyfikowanego podczas tworzenia nowego pliku. Coś w stylu:


// watcher_Created:
_currentlyObservedFiles.Add(e.Name);

// watcher_Changed:
if (_currentlyObservedFiles.Contains(e.Name))
{
_currentlyObservedFiles.Remove(e.Name);
OnAfterCreated(e);
}



A co do katalogu zamiast pliku... Do tej chwili byłem przekonany, że zostanie to wychwycone w FileNotFoundException. Okazuje się jednak, że jest wówczas wyrzucany UnauthorizedAccessException. Potem odpowiednio zaktualizuję posta.

Tom

9 maja 2008 15:12

No już doczytałem, przerwa spowodowana "Piaskowym Dziadygą"

apl

9 maja 2008 16:15

(...) do rozwiązania z Created trzeba by było dodać listę aktualnie obserwowanych plików tak, aby zdarzenie AfterCreated nie zostało wywołane dla już istniejącego pliku zmodyfikowanego podczas tworzenia nowego pliku

Niekoniecznie, wystarczy stworzyć prostą klasę:

private class FileChangedWatcher
{
public string FullPath { get; private set; }
public FileSystemWatcherEx FileSystemWatcher { get; private set; }

public FileChangedWatcher(FileSystemWatcherEx watcher, string path)
{
FullPath = path;
FileSystemWatcher = watcher;
}

public void Attach()
{
FileSystemWatcher.Changed += HandleFileChanged;
}

public void Unattach()
{
FileSystemWatcher.Changed -= HandleFileChanged;
}

private void NotifyFileReady()
{
FileSystemWatcher.NotifyFileReady(FullPath);
}

private void HandleFileChanged(object sender, FileSystemEventArgs e)
{
if (e.FullPath == FullPath) {
try {
File.Open(FullPath, FileMode.Open, FileAccess.Read, FileShare.None).Close();
Unattach();
NotifyFileReady();
}
catch (IOException) {
// Nic nie rób.
}
}
}
}

W klasie FileSystemWatcherEx będziemy potrzebować metody do powiadamiania obiektu o dostępności pliku:

private void NotifyFileReady(string fullPath)
{
string directoryName = Path.GetDirectoryName(fullPath);
string fileName = Path.GetFileName(fullPath);
OnAfterCreated(new FileSystemEventArgs(WatcherChangeTypes.Created, directoryName, fileName));
}

Metodę obsługi zdarzenia Created implementujemy w ten sposób:

private void HandleFileCreated(object sender, FileSystemEventArgs e)
{
if (File.Exists(e.FullPath)) {
FileSystemWatcherEx watcher = (FileSystemWatcherEx) sender;
FileChangedWatcher w = new FileChangedWatcher(watcher, e.FullPath);
w.Attach();
}
}

W konstruktorze podpinamy ją do odpowiedniego zdarzenia i to już w zasadzie wszystko:

public FileSystemWatcherEx()
{
Created += HandleFileCreated;
}

Praktycznie cały trik sprowadza się do wywołania metody Attach. Ponieważ tworzymy w niej delegat w oparciu o metodę niestatyczną, jest w nim zapamiętywana referencja do obiektu FileChangedWatcher, dla którego wywołano metodę Attach. Następnie sam delegat jest zapamiętywany przez obiekt FileSystemWatcher. W efekcie do momentu wywołania metody Unattach GC nie może zniszczyć obiektu FileChangedWatcher.

Zaznaczam, że jest to tylko pewna moja koncepcja, która niekoniecznie musi działać (kod nie był testowany, a na blogu nie widzę przycisku "Build All"), ale rozwiązania szukałbym gdzieś w tym kierunku.

Procent

9 maja 2008 17:31

Gdyby nie brak FullTrust na serwerze to dodanie przycisku BuildAll do bloga nie byłoby wielkim problemem:).

A propozycja dodatkowej klasy - ciekawa, OK i w ogóle, ale moim zdaniem w tym konkretnym przypadku strzelasz z armaty do wróbli.

apl

9 maja 2008 19:56

Kwestia podejścia, osobiście nie porównałbym tego rozwiązania ani do armaty, ani nawet do karabinu wielkokalibrowego, z którego w finale najnowszej części "Rambo" tytułowy bohater rozwala tabuny birmańskiej piechoty. Obydwa rozwiązania są tego samego kalibru, wszystko zależy od tego, co chcemy osiągnąć. Jeśli chcemy zastosować strategię push, wówczas polegamy na zdarzeniach zgłaszanych przez FileSystemWatcher. Jeśli chcemy zastosować strategię pull, wówczas stosujemy polling w nowym wątku. Pytanie, którą ze strategii uznajemy w tym przypadku za lepszą?

Jak już zdążyłeś zauważyć, skłaniam się ku rozwiązaniu w modelu push. Główną jego zaletą jest to, że o dostępności pliku zostaniemy poinformowani możliwie najwcześniej, jednocześnie implementacja jest nieskomplikowana. Jako główną wadę można postrzegać to, że jesteśmy ograniczeni przez obiekt FileSystemWatcher i wszelkie akcje musimy wykonywać pod jego dyktando. Podsumowując, jest to metoda prosta, precyzyjna, efektywna i głupia.

W modelu pull główną zaletą jest swoboda, która pozwala na implementację pewnych inteligentnych zachowań, np. możemy próbkować nierównomiernie, w zależności od tego, ile czasu minęło od rozpoczęcia całego proces. Rozwiązanie to może być skuteczne, lecz pod warunkiem, że jest adaptatywne i/lub dobrze dostrojone. Podsumowując, jest to co prawda metoda inteligentna, lecz równocześnie trudna, nieprecyzyjna i tylko czasami efektywna. Osobiście staram się unikać takiego podejścia, gdyż rzadko trafia się okazja, by naprawdę wykorzystać jego zalety.

Dodaj komentarz


 

[b][/b] - [i][/i] - [u][/u] - [quote][/quote] - [code][/code]