using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using RoboSharp.Interfaces;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using RoboSharp.EventArgObjects;
namespace RoboSharp.Results
{
///
/// Object that provides objects whose events can be bound to report estimated RoboCommand progress periodically.
///
/// Note: Only works properly with /V verbose set TRUE.
///
///
/// Subscribe to or to be notified when the ProgressEstimator becomes available for binding
/// Create event handler to subscribe to the Events you want to handle:
///
/// private void OnProgressEstimatorCreated(object sender, Results.ProgressEstimatorCreatedEventArgs e) {
/// e.ResultsEstimate.ByteStats.PropertyChanged += ByteStats_PropertyChanged;
/// e.ResultsEstimate.DirStats.PropertyChanged += DirStats_PropertyChanged;
/// e.ResultsEstimate.FileStats.PropertyChanged += FileStats_PropertyChanged;
/// }
///
///
///
///
public class ProgressEstimator : IProgressEstimator, IResults
{
#region < Constructors >
private ProgressEstimator() { }
internal ProgressEstimator(RoboCommand cmd)
{
command = cmd;
DirStatField = new Statistic(Statistic.StatType.Directories, "Directory Stats Estimate");
FileStatsField = new Statistic(Statistic.StatType.Files, "File Stats Estimate");
ByteStatsField = new Statistic(Statistic.StatType.Bytes, "Byte Stats Estimate");
tmpByte.EnablePropertyChangeEvent = false;
tmpFile.EnablePropertyChangeEvent = false;
tmpDir.EnablePropertyChangeEvent = false;
this.StartUpdateTask(out UpdateTaskCancelSource);
}
#endregion
#region < Private Members >
private readonly RoboCommand command;
private bool SkippingFile { get; set; }
private bool CopyOpStarted { get; set; }
internal bool FileFailed { get; set; }
private RoboSharpConfiguration Config => command?.Configuration;
// Stat Objects that will be publicly visible
private readonly Statistic DirStatField;
private readonly Statistic FileStatsField;
private readonly Statistic ByteStatsField;
internal enum WhereToAdd { Copied, Skipped, Extra, MisMatch, Failed }
// Storage for last entered Directory and File objects
/// Used for providing Source Directory in CopyProgressChanged args
internal ProcessedFileInfo CurrentDir { get; private set; }
/// Used for providing Source Directory in CopyProgressChanged args AND for byte Statistic
internal ProcessedFileInfo CurrentFile { get; private set; }
/// Marked as TRUE if this is LIST ONLY mode or the file is 0KB -- Value set during 'AddFile' method
private bool CurrentFile_SpecialHandling { get; set; }
//Stat objects to house the data temporarily before writing to publicly visible stat objects
readonly Statistic tmpDir =new Statistic(type: Statistic.StatType.Directories);
readonly Statistic tmpFile = new Statistic(type: Statistic.StatType.Files);
readonly Statistic tmpByte = new Statistic(type: Statistic.StatType.Bytes);
//UpdatePeriod
private const int UpdatePeriod = 150; // Update Period in milliseconds to push Updates to a UI or RoboQueueProgressEstimator
private readonly object DirLock = new object(); //Thread Lock for tmpDir
private readonly object FileLock = new object(); //Thread Lock for tmpFile and tmpByte
private readonly object UpdateLock = new object(); //Thread Lock for NextUpdatePush and UpdateTaskTrgger
private DateTime NextUpdatePush = DateTime.Now.AddMilliseconds(UpdatePeriod);
private TaskCompletionSource UpdateTaskTrigger; // TCS that the UpdateTask awaits on
private CancellationTokenSource UpdateTaskCancelSource; // While !Cancelled, UpdateTask continues looping
#endregion
#region < Public Properties >
///
/// Estimate of current number of directories processed while the job is still running.
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
///
public IStatistic DirectoriesStatistic => DirStatField;
///
/// Estimate of current number of files processed while the job is still running.
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
///
public IStatistic FilesStatistic => FileStatsField;
///
/// Estimate of current number of bytes processed while the job is still running.
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
///
public IStatistic BytesStatistic => ByteStatsField;
RoboCopyExitStatus IResults.Status => new RoboCopyExitStatus((int)GetExitCode());
///
public delegate void UIUpdateEventHandler(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e);
///
public event UIUpdateEventHandler ValuesUpdated;
#endregion
#region < Public Methods >
///
/// Parse this object's stats into a enum.
///
///
public RoboCopyExitCodes GetExitCode()
{
Results.RoboCopyExitCodes code = 0;
//Files Copied
if (FileStatsField.Copied > 0)
code |= Results.RoboCopyExitCodes.FilesCopiedSuccessful;
//Extra
if (DirStatField.Extras > 0 | FileStatsField.Extras > 0)
code |= Results.RoboCopyExitCodes.ExtraFilesOrDirectoriesDetected;
//MisMatch
if (DirStatField.Mismatch > 0 | FileStatsField.Mismatch > 0)
code |= Results.RoboCopyExitCodes.MismatchedDirectoriesDetected;
//Failed
if (DirStatField.Failed > 0 | FileStatsField.Failed > 0)
code |= Results.RoboCopyExitCodes.SomeFilesOrDirectoriesCouldNotBeCopied;
return code;
}
#endregion
#region < Get RoboCopyResults Object ( Internal ) >
///
/// Repackage the statistics into a new object
///
///
/// Used by ResultsBuilder as starting point for the results.
/// Should not be used anywhere else, as it kills the worker thread that calculates the Statistics objects.
///
///
internal RoboCopyResults GetResults()
{
//Stop the Update Task
UpdateTaskCancelSource?.Cancel();
UpdateTaskTrigger?.TrySetResult(null);
// - if copy operation wasn't completed, register it as failed instead.
// - if file was to be marked as 'skipped', then register it as skipped.
ProcessPreviousFile();
PushUpdate(); // Perform Final calculation before generating the Results Object
// Package up
return new RoboCopyResults()
{
BytesStatistic = (Statistic)BytesStatistic,
DirectoriesStatistic = (Statistic)DirectoriesStatistic,
FilesStatistic = (Statistic)FilesStatistic,
SpeedStatistic = new SpeedStatistic(),
};
}
#endregion
#region < Calculate Dirs (Internal) >
/// Increment
internal void AddDir(ProcessedFileInfo currentDir, bool CopyOperation)
{
WhereToAdd? whereTo = null;
bool SetCurrentDir = false;
if (currentDir.FileClass.Equals(Config.LogParsing_ExistingDir, StringComparison.CurrentCultureIgnoreCase)) // Existing Dir
{
whereTo = WhereToAdd.Skipped;
SetCurrentDir = true;
}
else if (currentDir.FileClass.Equals(Config.LogParsing_NewDir, StringComparison.CurrentCultureIgnoreCase)) //New Dir
{
whereTo = WhereToAdd.Copied;
SetCurrentDir = true;
}
else if (currentDir.FileClass.Equals(Config.LogParsing_ExtraDir, StringComparison.CurrentCultureIgnoreCase)) //Extra Dir
{
whereTo = WhereToAdd.Extra;
SetCurrentDir = false;
}
else if (currentDir.FileClass.Equals(Config.LogParsing_DirectoryExclusion, StringComparison.CurrentCultureIgnoreCase)) //Excluded Dir
{
whereTo = WhereToAdd.Skipped;
SetCurrentDir = false;
}
//Store CurrentDir under various conditions
if (SetCurrentDir) CurrentDir = currentDir;
lock (DirLock)
{
switch (whereTo)
{
case WhereToAdd.Copied: tmpDir.Total++; tmpDir.Copied++;break;
case WhereToAdd.Extra: tmpDir.Extras++; break; //Extras do not count towards total
case WhereToAdd.Failed: tmpDir.Total++; tmpDir.Failed++; break;
case WhereToAdd.MisMatch: tmpDir.Total++; tmpDir.Mismatch++; break;
case WhereToAdd.Skipped: tmpDir.Total++; tmpDir.Skipped++; break;
}
}
//Check if the UpdateTask should push an update to the public fields
if (Monitor.TryEnter(UpdateLock))
{
if (NextUpdatePush <= DateTime.Now)
UpdateTaskTrigger?.TrySetResult(null);
Monitor.Exit(UpdateLock);
}
}
#endregion
#region < Calculate Files (Internal) >
///
/// Performs final processing of the previous file if needed
///
private void ProcessPreviousFile()
{
if (CurrentFile != null)
{
if (FileFailed)
{
PerformByteCalc(CurrentFile, WhereToAdd.Failed);
}
else if (CopyOpStarted && CurrentFile_SpecialHandling)
{
PerformByteCalc(CurrentFile, WhereToAdd.Copied);
}
else if (SkippingFile)
{
PerformByteCalc(CurrentFile, WhereToAdd.Skipped);
}
else if (UpdateTaskCancelSource?.IsCancellationRequested ?? true)
{
//Default marks as failed - This should only occur during the 'GetResults()' method due to the if statement above.
PerformByteCalc(CurrentFile, WhereToAdd.Failed);
}
}
}
/// Increment
internal void AddFile(ProcessedFileInfo currentFile, bool CopyOperation)
{
ProcessPreviousFile();
CurrentFile = currentFile;
SkippingFile = false;
CopyOpStarted = false;
FileFailed = false;
// Flag to perform checks during a ListOnly operation OR for 0kb files (They won't get Progress update, but will be created)
bool SpecialHandling = !CopyOperation || currentFile.Size == 0;
CurrentFile_SpecialHandling = SpecialHandling;
// EXTRA FILES
if (currentFile.FileClass.Equals(Config.LogParsing_ExtraFile, StringComparison.CurrentCultureIgnoreCase))
{
PerformByteCalc(currentFile, WhereToAdd.Extra);
}
//MisMatch
else if (currentFile.FileClass.Equals(Config.LogParsing_MismatchFile, StringComparison.CurrentCultureIgnoreCase))
{
PerformByteCalc(currentFile, WhereToAdd.MisMatch);
}
//Failed Files
else if (currentFile.FileClass.Equals(Config.LogParsing_FailedFile, StringComparison.CurrentCultureIgnoreCase))
{
PerformByteCalc(currentFile, WhereToAdd.Failed);
}
//Files to be Copied/Skipped
else
{
SkippingFile = CopyOperation;//Assume Skipped, adjusted when CopyProgress is updated
if (currentFile.FileClass.Equals(Config.LogParsing_NewFile, StringComparison.CurrentCultureIgnoreCase)) // New File
{
//Special handling for 0kb files & ListOnly -> They won't get Progress update, but will be created
if (SpecialHandling)
{
SetCopyOpStarted();
}
}
else if (currentFile.FileClass.Equals(Config.LogParsing_SameFile, StringComparison.CurrentCultureIgnoreCase)) //Identical Files
{
if (command.SelectionOptions.IncludeSame)
{
if (SpecialHandling) SetCopyOpStarted(); // Only add to Copied if ListOnly / 0-bytes
}
else
PerformByteCalc(currentFile, WhereToAdd.Skipped);
}
else if (SpecialHandling) // These checks are always performed during a ListOnly operation
{
switch (true)
{
//Skipped Or Copied Conditions
case true when currentFile.FileClass.Equals(Config.LogParsing_NewerFile, StringComparison.CurrentCultureIgnoreCase): // ExcludeNewer
SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeNewer);
break;
case true when currentFile.FileClass.Equals(Config.LogParsing_OlderFile, StringComparison.CurrentCultureIgnoreCase): // ExcludeOlder
SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeOlder);
break;
case true when currentFile.FileClass.Equals(Config.LogParsing_ChangedExclusion, StringComparison.CurrentCultureIgnoreCase): //ExcludeChanged
SkippedOrCopied(currentFile, command.SelectionOptions.ExcludeChanged);
break;
case true when currentFile.FileClass.Equals(Config.LogParsing_TweakedInclusion, StringComparison.CurrentCultureIgnoreCase): //IncludeTweaked
SkippedOrCopied(currentFile, !command.SelectionOptions.IncludeTweaked);
break;
//Mark As Skip Conditions
case true when currentFile.FileClass.Equals(Config.LogParsing_FileExclusion, StringComparison.CurrentCultureIgnoreCase): //FileExclusion
case true when currentFile.FileClass.Equals(Config.LogParsing_AttribExclusion, StringComparison.CurrentCultureIgnoreCase): //AttributeExclusion
case true when currentFile.FileClass.Equals(Config.LogParsing_MaxFileSizeExclusion, StringComparison.CurrentCultureIgnoreCase): //MaxFileSizeExclusion
case true when currentFile.FileClass.Equals(Config.LogParsing_MinFileSizeExclusion, StringComparison.CurrentCultureIgnoreCase): //MinFileSizeExclusion
case true when currentFile.FileClass.Equals(Config.LogParsing_MaxAgeOrAccessExclusion, StringComparison.CurrentCultureIgnoreCase): //MaxAgeOrAccessExclusion
case true when currentFile.FileClass.Equals(Config.LogParsing_MinAgeOrAccessExclusion, StringComparison.CurrentCultureIgnoreCase): //MinAgeOrAccessExclusion
PerformByteCalc(currentFile, WhereToAdd.Skipped);
break;
}
}
}
}
///
/// Method meant only to be called from AddFile method while SpecialHandling is true - helps normalize code and avoid repetition
///
private void SkippedOrCopied(ProcessedFileInfo currentFile, bool MarkSkipped)
{
if (MarkSkipped)
PerformByteCalc(currentFile, WhereToAdd.Skipped);
else
{
SetCopyOpStarted();
//PerformByteCalc(currentFile, WhereToAdd.Copied);
}
}
/// Catch start copy progress of large files
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
internal void SetCopyOpStarted()
{
SkippingFile = false;
CopyOpStarted = true;
}
/// Increment .Copied ( Triggered when copy progress = 100% )
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
internal void AddFileCopied(ProcessedFileInfo currentFile)
{
PerformByteCalc(currentFile, WhereToAdd.Copied);
}
///
/// Perform the calculation for the ByteStatistic
///
private void PerformByteCalc(ProcessedFileInfo file, WhereToAdd where)
{
if (file == null) return;
//Reset Flags
SkippingFile = false;
CopyOpStarted = false;
FileFailed = false;
CurrentFile = null;
CurrentFile_SpecialHandling = false;
//Perform Math
lock (FileLock)
{
//Extra files do not contribute towards Copy Total.
if (where == WhereToAdd.Extra)
{
tmpFile.Extras++;
tmpByte.Extras += file.Size;
}
else
{
tmpFile.Total++;
tmpByte.Total += file.Size;
switch (where)
{
case WhereToAdd.Copied:
tmpFile.Copied++;
tmpByte.Copied += file.Size;
break;
case WhereToAdd.Extra:
break;
case WhereToAdd.Failed:
tmpFile.Failed++;
tmpByte.Failed += file.Size;
break;
case WhereToAdd.MisMatch:
tmpFile.Mismatch++;
tmpByte.Mismatch += file.Size;
break;
case WhereToAdd.Skipped:
tmpFile.Skipped++;
tmpByte.Skipped += file.Size;
break;
}
}
}
//Check if the UpdateTask should push an update to the public fields
if (Monitor.TryEnter(UpdateLock))
{
if (NextUpdatePush <= DateTime.Now)
UpdateTaskTrigger?.TrySetResult(null);
Monitor.Exit(UpdateLock);
}
}
#endregion
#region < PushUpdate to Public Stat Objects >
///
/// Creates a LongRunning task that is meant to periodically push out Updates to the UI on a thread isolated from the event thread.
///
///
///
private Task StartUpdateTask(out CancellationTokenSource CancelSource)
{
CancelSource = new CancellationTokenSource();
var CS = CancelSource;
return Task.Run(async () =>
{
while (!CS.IsCancellationRequested)
{
lock(UpdateLock)
{
PushUpdate();
UpdateTaskTrigger = new TaskCompletionSource();
NextUpdatePush = DateTime.Now.AddMilliseconds(UpdatePeriod);
}
await UpdateTaskTrigger.Task;
}
//Cleanup
CS?.Dispose();
UpdateTaskTrigger = null;
UpdateTaskCancelSource = null;
}, CS.Token);
}
///
/// Push the update to the public Stat Objects
///
private void PushUpdate()
{
//Lock the Stat objects, clone, reset them, then push the update to the UI.
Statistic TD = null;
Statistic TB = null;
Statistic TF = null;
lock (DirLock)
{
if (tmpDir.NonZeroValue)
{
TD = tmpDir.Clone();
tmpDir.Reset();
}
}
lock (FileLock)
{
if (tmpFile.NonZeroValue)
{
TF = tmpFile.Clone();
tmpFile.Reset();
}
if (tmpByte.NonZeroValue)
{
TB = tmpByte.Clone();
tmpByte.Reset();
}
}
//Push UI update after locks are released, to avoid holding up the other thread for too long
if (TD != null) DirStatField.AddStatistic(TD);
if (TB != null) ByteStatsField.AddStatistic(TB);
if (TF != null) FileStatsField.AddStatistic(TF);
//Raise the event if any of the values have been updated
if (TF != null || TD != null || TB != null)
{
ValuesUpdated?.Invoke(this, new IProgressEstimatorUpdateEventArgs(this, TB, TF, TD));
}
}
#endregion
}
}