// 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);
}
}
}
}