// Spludlow Software // Copyright © Samuel P. Ludlow 2020 All Rights Reserved // Distributed under the terms of the GNU General Public License version 3 // Distributed WITHOUT ANY WARRANTY; without implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE // https://www.spludlow.co.uk/LICENCE.TXT // The Spludlow logo is a registered trademark of Samuel P. Ludlow and may not be used without permission // v1.14 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Collections.Concurrent; using System.Timers; namespace Spludlow { /// /// Directory Watcher, run Watch() as a method server, specify ATM of method to call with single string parameter as the filename, the method should delete the file when finished. /// Your method should run in process (use empty quotes "" for the address) /// Starts by combing the directory and then using a FileSystemWatcher on creates to call the Submit() method each on a separate thread. /// The Submit() thread will run a sleep loop while the file is locked and then add the filenames to a BlockingCollection. /// The main processing loop will then Take from the BlockingCollection one at a time then call the specified ATM for a filename. /// Sleeps on separate threads for locked files. /// Only processes (executes the specified ATM) one at a time. /// The only responsibility of the called method is to delete the file before returning. /// Clean exit, when Stop() is called current method call will complete then exit. /// Will periodically call a fail-safe directory comb on a timer thread to catch anything missed or previously failed. /// public class DirectoryWatcher { private string _Directory = null; private BlockingCollection _BlockingCollection = null; private FileSystemWatcher _FileSystemWatcher = null; private Timer _Timer = null; public void Stop() { _BlockingCollection.CompleteAdding(); _FileSystemWatcher.EnableRaisingEvents = false; _Timer.Stop(); } /// /// Call this as a Spludlow-Service Method Server, use an empty address to run in process /// public void Watch(string directory, string address, string assembly, string type, string method) { _Directory = directory; _BlockingCollection = new BlockingCollection(); // Create and start watcher _FileSystemWatcher = new FileSystemWatcher(this._Directory); _FileSystemWatcher.IncludeSubdirectories = false; _FileSystemWatcher.NotifyFilter = NotifyFilters.FileName; _FileSystemWatcher.InternalBufferSize = 32 * 1024; // 64K max _FileSystemWatcher.Created += Watcher_Created; _FileSystemWatcher.EnableRaisingEvents = true; // Submit all existing CombDirectory(); // Fail Safe timer _Timer = new Timer(); _Timer.Interval = 3 * 60 * 1000; _Timer.AutoReset = true; _Timer.Elapsed += Timer_Elapsed; _Timer.Start(); Spludlow.Log.Report("DirectoryWatcher Started: " + this._Directory); // Main loop - take items from the collection string filename; while ((filename = Take()) != null) { if (File.Exists(filename) == false) continue; // Make the call try { Spludlow.CallMethod callMethod = new CallMethod(address, assembly, type, method, new object[] { filename }); Spludlow.Call.Run(callMethod); } catch (Exception ee) { Spludlow.Log.Error("DirectoryWatcher Call Error: " + filename, ee); } } _FileSystemWatcher.Dispose(); _BlockingCollection.Dispose(); _BlockingCollection = null; Spludlow.Log.Finish("DirectoryWatcher Stopped: " + this._Directory); } /// /// If CompleteAdding() has been called the blocked (nothing in collection) or next Take() will throw InvalidOperationException /// private string Take() { if (_BlockingCollection.IsAddingCompleted == true) return null; try { return _BlockingCollection.Take(); } catch (InvalidOperationException) { return null; } } private void Timer_Elapsed(object sender, ElapsedEventArgs e) { CombDirectory(); } private void CombDirectory() { string[] filenames = Directory.GetFiles(this._Directory); for (int index = 0; index < filenames.Length; ++index) { string filename = filenames[index]; Task.Run(() => Submit(filename)); } } private void Watcher_Created(object sender, FileSystemEventArgs e) { Task.Run(() => Submit(e.FullPath)); } private void Submit(string filename) { try { if (_BlockingCollection == null || _BlockingCollection.Contains(filename) == true || File.Exists(filename) == false) return; // While file locked int sleepSeconds = 1; while (Spludlow.Io.Files.IsOpen(filename) == true) { System.Threading.Thread.Sleep(sleepSeconds * 1000); sleepSeconds *= 2; // Double wait time each time if (_BlockingCollection == null || _BlockingCollection.Contains(filename) == true || File.Exists(filename) == false) return; } } catch (Exception ee) { Spludlow.Log.Error("DirectoryWatcher Submit: " + filename, ee); return; } try { _BlockingCollection.Add(filename); } catch (InvalidOperationException) { return; } } /// /// Test Pickup method to use with Spludlow.Io.TestFiles.DirectoryWritesSHA1() /// public static void TestPickup(string filename) { try { if (File.Exists(filename) == false) throw new ApplicationException("file does not exists"); if (Spludlow.Io.Files.IsOpen(filename) == true) throw new ApplicationException("file is open"); string hash = Spludlow.Hashing.SHA1HexFile(filename); if (Path.GetFileNameWithoutExtension(filename).EndsWith(hash) == false) throw new ApplicationException("bad hash"); File.Delete(filename); if (File.Exists(filename) == true) throw new ApplicationException("file not deleted"); Spludlow.Log.Info("TestPickup: " + filename); } catch (Exception ee) { throw new ApplicationException("TestPickup: " + filename + ", " + ee.Message, ee); } } } }