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: }
Zdarzenie [b]Create[/b] 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 [b]FileSystemWatcher[/b] – po zdarzeniu [b]Created[/b] wywoływane jest [i]przynajmniej raz[/i] zdarzenie [b]Changed[/b]. Niestety nie da się przewidzieć, ile takich zdarzeń wystąpi podczas tworzenia pliku, można natomiast w metodzie obsługi zdarzenia [b]Created[/b] podpiąć się pod zdarzenie [b]Changed[/b] i w nim sprawdzać, czy plik da się otworzyć, najlepiej żądając prawa dostępu do pliku na wyłączność (tj. wywołując [b]File.Open([i]ścieżka[/i], FileMode.Open, FileAccess.Read, FileShare.None)[/b]). Gdy żądanie zostanie spełnione, można odpiąć handler i wywołać zdarzenie informujące o dostępności pliku.
:) No i nie przeczytam drugiego akapitu :)
@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? ;)
@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:
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.
No już doczytałem, przerwa spowodowana "Piaskowym Dziadygą"
[i](…) 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[/i]
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 [b]FileSystemWatcherEx[/b] 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 [b]Created[/b] 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 [b]Attach[/b]. Ponieważ tworzymy w niej delegat w oparciu o metodę niestatyczną, jest w nim zapamiętywana referencja do obiektu [b]FileChangedWatcher[/b], dla którego wywołano metodę [b]Attach[/b]. Następnie sam delegat jest zapamiętywany przez obiekt [b]FileSystemWatcher[/b]. W efekcie do momentu wywołania metody [b]Unattach[/b] GC nie może zniszczyć obiektu [b]FileChangedWatcher[/b].
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.
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.
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ę [i]push[/i], wówczas polegamy na zdarzeniach zgłaszanych przez [b]FileSystemWatcher[/b]. Jeśli chcemy zastosować strategię [i]pull[/i], 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 [b]FileSystemWatcher[/b] 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.
nie znam c# a potrzebowałbym przetłumaczyć taką klasę na c++ niestety nie rozumiem konstrukcji
base.Created += (sender, e) =>
czy może ktoś napisać o tym kilka zdań / wskazać miejsce gdzie znajdę o tym info??
@mick:
Kluczowe pojęcia: delegaty (delegates) i zdarzenia (events), tutaj jest art w kontekście C++: http://www.functionx.com/vcnet/topics/delevents.htm . Z kolei (sender, e) => to wyrażenie lambda, nie wiem czy jest w c++ odpowiednik takiej konstrukcji… ale możesz zamiast tego napisać zwykłą metodę która przyjmuje argumenty object sender i EventArgs e.