Files
FSI.BT.IR.Tools/RoboSharp/Results/ProgressEstimator.cs
Maier Stephan SI d01747f75a Sicherung
2023-01-02 04:33:49 +01:00

522 lines
22 KiB
C#

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
{
/// <summary>
/// Object that provides <see cref="IStatistic"/> objects whose events can be bound to report estimated RoboCommand progress periodically.
/// <br/>
/// Note: Only works properly with /V verbose set TRUE.
/// </summary>
/// <remarks>
/// Subscribe to <see cref="RoboCommand.OnProgressEstimatorCreated"/> or <see cref="RoboQueue.OnProgressEstimatorCreated"/> to be notified when the ProgressEstimator becomes available for binding <br/>
/// Create event handler to subscribe to the Events you want to handle: <para/>
/// <code>
/// private void OnProgressEstimatorCreated(object sender, Results.ProgressEstimatorCreatedEventArgs e) { <br/>
/// e.ResultsEstimate.ByteStats.PropertyChanged += ByteStats_PropertyChanged;<br/>
/// e.ResultsEstimate.DirStats.PropertyChanged += DirStats_PropertyChanged;<br/>
/// e.ResultsEstimate.FileStats.PropertyChanged += FileStats_PropertyChanged;<br/>
/// }<br/>
/// </code>
/// <para/>
/// <see href="https://github.com/tjscience/RoboSharp/wiki/ProgressEstimator"/>
/// </remarks>
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
/// <summary>Used for providing Source Directory in CopyProgressChanged args</summary>
internal ProcessedFileInfo CurrentDir { get; private set; }
/// <summary>Used for providing Source Directory in CopyProgressChanged args AND for byte Statistic</summary>
internal ProcessedFileInfo CurrentFile { get; private set; }
/// <summary> Marked as TRUE if this is LIST ONLY mode or the file is 0KB -- Value set during 'AddFile' method </summary>
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<object> UpdateTaskTrigger; // TCS that the UpdateTask awaits on
private CancellationTokenSource UpdateTaskCancelSource; // While !Cancelled, UpdateTask continues looping
#endregion
#region < Public Properties >
/// <summary>
/// Estimate of current number of directories processed while the job is still running. <br/>
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
/// </summary>
public IStatistic DirectoriesStatistic => DirStatField;
/// <summary>
/// Estimate of current number of files processed while the job is still running. <br/>
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
/// </summary>
public IStatistic FilesStatistic => FileStatsField;
/// <summary>
/// Estimate of current number of bytes processed while the job is still running. <br/>
/// Estimate is provided by parsing of the LogLines produces by RoboCopy.
/// </summary>
public IStatistic BytesStatistic => ByteStatsField;
RoboCopyExitStatus IResults.Status => new RoboCopyExitStatus((int)GetExitCode());
/// <summary> </summary>
public delegate void UIUpdateEventHandler(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e);
/// <inheritdoc cref="IProgressEstimator.ValuesUpdated"/>
public event UIUpdateEventHandler ValuesUpdated;
#endregion
#region < Public Methods >
/// <summary>
/// Parse this object's stats into a <see cref="RoboCopyExitCodes"/> enum.
/// </summary>
/// <returns></returns>
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 ) >
/// <summary>
/// Repackage the statistics into a new <see cref="RoboCopyResults"/> object
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <returns></returns>
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) >
/// <summary>Increment <see cref="DirStatField"/></summary>
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) >
/// <summary>
/// Performs final processing of the previous file if needed
/// </summary>
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);
}
}
}
/// <summary>Increment <see cref="FileStatsField"/></summary>
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;
}
}
}
}
/// <summary>
/// Method meant only to be called from AddFile method while SpecialHandling is true - helps normalize code and avoid repetition
/// </summary>
private void SkippedOrCopied(ProcessedFileInfo currentFile, bool MarkSkipped)
{
if (MarkSkipped)
PerformByteCalc(currentFile, WhereToAdd.Skipped);
else
{
SetCopyOpStarted();
//PerformByteCalc(currentFile, WhereToAdd.Copied);
}
}
/// <summary>Catch start copy progress of large files</summary>
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
internal void SetCopyOpStarted()
{
SkippingFile = false;
CopyOpStarted = true;
}
/// <summary>Increment <see cref="FileStatsField"/>.Copied ( Triggered when copy progress = 100% ) </summary>
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
internal void AddFileCopied(ProcessedFileInfo currentFile)
{
PerformByteCalc(currentFile, WhereToAdd.Copied);
}
/// <summary>
/// Perform the calculation for the ByteStatistic
/// </summary>
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 >
/// <summary>
/// Creates a LongRunning task that is meant to periodically push out Updates to the UI on a thread isolated from the event thread.
/// </summary>
/// <param name="CancelSource"></param>
/// <returns></returns>
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<object>();
NextUpdatePush = DateTime.Now.AddMilliseconds(UpdatePeriod);
}
await UpdateTaskTrigger.Task;
}
//Cleanup
CS?.Dispose();
UpdateTaskTrigger = null;
UpdateTaskCancelSource = null;
}, CS.Token);
}
/// <summary>
/// Push the update to the public Stat Objects
/// </summary>
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
}
}