Squashed 'FSI.Lib/' changes from 6aa4846..4a27cd3
4a27cd3 RoboSharp eingefügt 1b2fc1f Erweiterungsmethode für Startparameter einefügt git-subtree-dir: FSI.Lib git-subtree-split: 4a27cd377a1959dc669625473b018e42c31ef147
This commit is contained in:
521
FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs
Normal file
521
FSI.Lib/Tools/RoboSharp/Results/ProgressEstimator.cs
Normal file
@@ -0,0 +1,521 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FSI.Lib.Tools.RoboSharp.EventArgObjects;
|
||||
|
||||
namespace FSI.Lib.Tools.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
|
||||
}
|
||||
}
|
||||
127
FSI.Lib/Tools/RoboSharp/Results/ResultsBuilder.cs
Normal file
127
FSI.Lib/Tools/RoboSharp/Results/ResultsBuilder.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to build a <see cref="RoboCopyResults"/> object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/ResultsBuilder"/>
|
||||
/// </remarks>
|
||||
internal class ResultsBuilder
|
||||
{
|
||||
private ResultsBuilder() { }
|
||||
|
||||
internal ResultsBuilder(RoboCommand roboCommand) {
|
||||
RoboCommand = roboCommand;
|
||||
Estimator = new ProgressEstimator(roboCommand);
|
||||
}
|
||||
|
||||
#region < Private Members >
|
||||
|
||||
///<summary>Reference back to the RoboCommand that spawned this object</summary>
|
||||
private readonly RoboCommand RoboCommand;
|
||||
|
||||
private readonly List<string> outputLines = new List<string>();
|
||||
|
||||
/// <summary>This is the last line that was logged.</summary>
|
||||
internal string LastLine => outputLines.Count > 0 ? outputLines.Last() : "";
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Command Options Properties >
|
||||
|
||||
/// <see cref="RoboCommand.CommandOptions"/>
|
||||
internal string CommandOptions { get; set; }
|
||||
|
||||
/// <inheritdoc cref="CopyOptions.Source"/>
|
||||
internal string Source { get; set; }
|
||||
|
||||
/// <inheritdoc cref="CopyOptions.Destination"/>
|
||||
internal string Destination { get; set; }
|
||||
|
||||
internal List<ErrorEventArgs> RoboCopyErrors { get; } = new List<ErrorEventArgs>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Counters in case cancellation >
|
||||
|
||||
/// <inheritdoc cref="ProgressEstimator"/>
|
||||
internal ProgressEstimator Estimator { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Add a LogLine reported by RoboCopy to the LogLines list.
|
||||
/// </summary>
|
||||
internal void AddOutput(string output)
|
||||
{
|
||||
if (output == null)
|
||||
return;
|
||||
|
||||
if (Regex.IsMatch(output, @"^\s*[\d\.,]+%\s*$", RegexOptions.Compiled)) //Ignore Progress Indicators
|
||||
return;
|
||||
|
||||
outputLines.Add(output);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the results from parsing the logLines.
|
||||
/// </summary>
|
||||
/// <param name="exitCode"></param>
|
||||
/// <param name="IsProgressUpdateEvent">This is used by the ProgressUpdateEventArgs to ignore the loglines when generating the estimate </param>
|
||||
/// <returns></returns>
|
||||
internal RoboCopyResults BuildResults(int exitCode, bool IsProgressUpdateEvent = false)
|
||||
{
|
||||
var res = Estimator.GetResults(); //Start off with the estimated results, and replace if able
|
||||
|
||||
res.JobName = RoboCommand.Name;
|
||||
res.Status = new RoboCopyExitStatus(exitCode);
|
||||
|
||||
var statisticLines = IsProgressUpdateEvent ? new List<string>() : GetStatisticLines();
|
||||
|
||||
//Dir Stats
|
||||
if (exitCode >= 0 && statisticLines.Count >= 1)
|
||||
res.DirectoriesStatistic = Statistic.Parse(Statistic.StatType.Directories, statisticLines[0]);
|
||||
|
||||
//File Stats
|
||||
if (exitCode >= 0 && statisticLines.Count >= 2)
|
||||
res.FilesStatistic = Statistic.Parse(Statistic.StatType.Files, statisticLines[1]);
|
||||
|
||||
//Bytes
|
||||
if (exitCode >= 0 && statisticLines.Count >= 3)
|
||||
res.BytesStatistic = Statistic.Parse(Statistic.StatType.Bytes, statisticLines[2]);
|
||||
|
||||
//Speed Stats
|
||||
if (exitCode >= 0 && statisticLines.Count >= 6)
|
||||
res.SpeedStatistic = SpeedStatistic.Parse(statisticLines[4], statisticLines[5]);
|
||||
|
||||
res.LogLines = outputLines.ToArray();
|
||||
res.RoboCopyErrors = this.RoboCopyErrors.ToArray();
|
||||
res.Source = this.Source;
|
||||
res.Destination = this.Destination;
|
||||
res.CommandOptions = this.CommandOptions;
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<string> GetStatisticLines()
|
||||
{
|
||||
var res = new List<string>();
|
||||
for (var i = outputLines.Count-1; i > 0; i--)
|
||||
{
|
||||
var line = outputLines[i];
|
||||
if (line.StartsWith("-----------------------"))
|
||||
break;
|
||||
|
||||
if (line.Contains(":") && !line.Contains("\\"))
|
||||
res.Add(line);
|
||||
}
|
||||
|
||||
res.Reverse();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
FSI.Lib/Tools/RoboSharp/Results/RoboCopyExitCodes.cs
Normal file
42
FSI.Lib/Tools/RoboSharp/Results/RoboCopyExitCodes.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// RoboCopy Exit Codes
|
||||
/// </summary>
|
||||
/// <remarks><see href="https://ss64.com/nt/robocopy-exit.html"/></remarks>
|
||||
[Flags]
|
||||
public enum RoboCopyExitCodes
|
||||
{
|
||||
/// <summary>No Files Copied, No Errors Occured</summary>
|
||||
NoErrorNoCopy = 0x0,
|
||||
/// <summary>One or more files were copied successfully</summary>
|
||||
FilesCopiedSuccessful = 0x1,
|
||||
/// <summary>
|
||||
/// Some Extra files or directories were detected.<br/>
|
||||
/// Examine the output log for details.
|
||||
/// </summary>
|
||||
ExtraFilesOrDirectoriesDetected = 0x2,
|
||||
/// <summary>
|
||||
/// Some Mismatched files or directories were detected.<br/>
|
||||
/// Examine the output log. Housekeeping might be required.
|
||||
/// </summary>
|
||||
MismatchedDirectoriesDetected = 0x4,
|
||||
/// <summary>
|
||||
/// Some files or directories could not be copied <br/>
|
||||
/// (copy errors occurred and the retry limit was exceeded).
|
||||
/// Check these errors further.
|
||||
/// </summary>
|
||||
SomeFilesOrDirectoriesCouldNotBeCopied = 0x8,
|
||||
/// <summary>
|
||||
/// Serious error. Robocopy did not copy any files.<br/>
|
||||
/// Either a usage error or an error due to insufficient access privileges on the source or destination directories.
|
||||
/// </summary>
|
||||
SeriousErrorOccurred = 0x10,
|
||||
/// <summary>
|
||||
/// The Robocopy process exited prior to completion
|
||||
/// </summary>
|
||||
Cancelled = -1,
|
||||
}
|
||||
}
|
||||
103
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResults.cs
Normal file
103
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResults.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Results provided by the RoboCopy command. Includes the Log, Exit Code, and statistics parsed from the log.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/RoboCopyResults"/>
|
||||
/// </remarks>
|
||||
public class RoboCopyResults : IResults, ITimeSpan
|
||||
{
|
||||
internal RoboCopyResults() { }
|
||||
|
||||
#region < Properties >
|
||||
|
||||
/// <inheritdoc cref="CopyOptions.Source"/>
|
||||
public string Source { get; internal set; }
|
||||
|
||||
/// <inheritdoc cref="CopyOptions.Destination"/>
|
||||
public string Destination { get; internal set; }
|
||||
|
||||
/// <inheritdoc cref="RoboCommand.CommandOptions"/>
|
||||
public string CommandOptions { get; internal set; }
|
||||
|
||||
/// <inheritdoc cref="RoboCommand.Name"/>
|
||||
public string JobName { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// All Errors that were generated by RoboCopy during the run.
|
||||
/// </summary>
|
||||
public ErrorEventArgs[] RoboCopyErrors{ get; internal set; }
|
||||
|
||||
/// <inheritdoc cref="RoboCopyExitStatus"/>
|
||||
public RoboCopyExitStatus Status { get; internal set; }
|
||||
|
||||
/// <summary> Information about number of Directories Copied, Skipped, Failed, etc.</summary>
|
||||
/// <remarks>
|
||||
/// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy. <br/>
|
||||
/// Results should only be treated as accurate if <see cref="Status"/>.ExitCodeValue >= 0 and the job was run with <see cref="LoggingOptions.NoJobSummary"/> = FALSE
|
||||
/// </remarks>
|
||||
public Statistic DirectoriesStatistic { get; internal set; }
|
||||
|
||||
/// <summary> Information about number of Files Copied, Skipped, Failed, etc.</summary>
|
||||
/// <remarks>
|
||||
/// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy. <br/>
|
||||
/// Results should only be treated as accurate if <see cref="Status"/>.ExitCodeValue >= 0 and the job was run with <see cref="LoggingOptions.NoJobSummary"/> = FALSE
|
||||
/// </remarks>
|
||||
public Statistic FilesStatistic { get; internal set; }
|
||||
|
||||
/// <summary> Information about number of Bytes processed.</summary>
|
||||
/// <remarks>
|
||||
/// If the job was cancelled, or run without a Job Summary, this will attempt to provide approximate results based on the Process.StandardOutput from Robocopy. <br/>
|
||||
/// Results should only be treated as accurate if <see cref="Status"/>.ExitCodeValue >= 0 and the job was run with <see cref="LoggingOptions.NoJobSummary"/> = FALSE
|
||||
/// </remarks>
|
||||
public Statistic BytesStatistic { get; internal set; }
|
||||
|
||||
/// <inheritdoc cref="RoboSharp.Results.SpeedStatistic"/>
|
||||
public SpeedStatistic SpeedStatistic { get; internal set; }
|
||||
|
||||
/// <summary> Output Text reported by RoboCopy </summary>
|
||||
public string[] LogLines { get; internal set; }
|
||||
|
||||
/// <summary> Time the RoboCopy process was started </summary>
|
||||
public DateTime StartTime { get; internal set; }
|
||||
|
||||
/// <summary> Time the RoboCopy process was completed / cancelled. </summary>
|
||||
public DateTime EndTime { get; internal set; }
|
||||
|
||||
/// <summary> Length of Time the RoboCopy Process ran </summary>
|
||||
public TimeSpan TimeSpan { get; internal set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region < IResults >
|
||||
|
||||
IStatistic IResults.BytesStatistic => BytesStatistic;
|
||||
IStatistic IResults.DirectoriesStatistic => DirectoriesStatistic;
|
||||
IStatistic IResults.FilesStatistic => FilesStatistic;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current object.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A string that represents the current object.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
string str = $"ExitCode: {Status.ExitCode}, Directories: {DirectoriesStatistic?.Total.ToString() ?? "Unknown"}, Files: {FilesStatistic?.Total.ToString() ?? "Unknown"}, Bytes: {BytesStatistic?.Total.ToString() ?? "Unknown"}";
|
||||
|
||||
if (SpeedStatistic != null)
|
||||
{
|
||||
str += $", Speed: {SpeedStatistic.BytesPerSec} Bytes/sec";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
396
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsList.cs
Normal file
396
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsList.cs
Normal file
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
//using System.Linq;
|
||||
using System.Text;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using FSI.Lib.Tools.RoboSharp.EventArgObjects;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
using StatType = FSI.Lib.Tools.RoboSharp.Results.Statistic.StatType;
|
||||
using System.Collections;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Object used to represent results from multiple <see cref="RoboCommand"/>s. <br/>
|
||||
/// As <see cref="RoboCopyResults"/> are added to this object, it will update the Totals and Averages accordingly.<para/>
|
||||
/// Implements:
|
||||
/// <br/><see cref="IRoboCopyResultsList"/>
|
||||
/// <br/><see cref="IList{RoboCopyResults}"/> where T = RoboCopyResults
|
||||
/// <br/><see cref="INotifyCollectionChanged"/>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/RoboCopyResultsList"/>
|
||||
/// </remarks>
|
||||
public sealed class RoboCopyResultsList : IRoboCopyResultsList, IList<RoboCopyResults>, INotifyCollectionChanged
|
||||
{
|
||||
#region < Constructors >
|
||||
|
||||
/// <inheritdoc cref="List{T}.List()"/>
|
||||
public RoboCopyResultsList() { InitCollection(null); Init(); }
|
||||
|
||||
/// <param name="result">Populate the new List object with this result as the first item.</param>
|
||||
/// <inheritdoc cref="List{T}.List(IEnumerable{T})"/>
|
||||
public RoboCopyResultsList(RoboCopyResults result) { ResultsList.Add(result); InitCollection(null); Init(); }
|
||||
|
||||
/// <inheritdoc cref="List{T}.List(IEnumerable{T})"/>
|
||||
public RoboCopyResultsList(IEnumerable<RoboCopyResults> collection) { InitCollection(collection); Init(); }
|
||||
|
||||
/// <summary>
|
||||
/// Clone a RoboCopyResultsList into a new object
|
||||
/// </summary>
|
||||
public RoboCopyResultsList(RoboCopyResultsList resultsList)
|
||||
{
|
||||
InitCollection(resultsList);
|
||||
Total_DirStatsField = GetLazyStat(resultsList.Total_DirStatsField, GetLazyFunc(GetDirectoriesStatistics, StatType.Directories));
|
||||
Total_FileStatsField = GetLazyStat(resultsList.Total_FileStatsField, GetLazyFunc(GetFilesStatistics, StatType.Files));
|
||||
Total_ByteStatsField = GetLazyStat(resultsList.Total_ByteStatsField, GetLazyFunc(GetByteStatistics, StatType.Bytes));
|
||||
Average_SpeedStatsField = GetLazyStat(resultsList.Average_SpeedStatsField, GetLazyAverageSpeedFunc());
|
||||
ExitStatusSummaryField = GetLazyStat(resultsList.ExitStatusSummaryField, GetLazCombinedStatusFunc());
|
||||
}
|
||||
|
||||
#region < Constructor Helper Methods >
|
||||
|
||||
private void InitCollection(IEnumerable<RoboCopyResults> collection)
|
||||
{
|
||||
ResultsList.AddRange(collection);
|
||||
ResultsList.CollectionChanged += OnCollectionChanged;
|
||||
}
|
||||
|
||||
private void Init()
|
||||
{
|
||||
Total_DirStatsField = new Lazy<Statistic>(GetLazyFunc(GetDirectoriesStatistics, StatType.Directories));
|
||||
Total_FileStatsField = new Lazy<Statistic>(GetLazyFunc(GetFilesStatistics, StatType.Files));
|
||||
Total_ByteStatsField = new Lazy<Statistic>(GetLazyFunc(GetByteStatistics, StatType.Bytes));
|
||||
Average_SpeedStatsField = new Lazy<AverageSpeedStatistic>(GetLazyAverageSpeedFunc());
|
||||
ExitStatusSummaryField = new Lazy<RoboCopyCombinedExitStatus>(GetLazCombinedStatusFunc());
|
||||
}
|
||||
|
||||
private Func<Statistic> GetLazyFunc(Func<IStatistic[]> Action, StatType StatType) => new Func<Statistic>(() => Statistic.AddStatistics(Action.Invoke(), StatType));
|
||||
private Func<AverageSpeedStatistic> GetLazyAverageSpeedFunc() => new Func<AverageSpeedStatistic>(() => AverageSpeedStatistic.GetAverage(GetSpeedStatistics()));
|
||||
private Func<RoboCopyCombinedExitStatus> GetLazCombinedStatusFunc() => new Func<RoboCopyCombinedExitStatus>(() => RoboCopyCombinedExitStatus.CombineStatuses(GetStatuses()));
|
||||
private Lazy<T> GetLazyStat<T>(Lazy<T> lazyStat, Func<T> action) where T : ICloneable
|
||||
{
|
||||
if (lazyStat.IsValueCreated)
|
||||
{
|
||||
var clone = lazyStat.Value.Clone();
|
||||
return new Lazy<T>(() => (T)clone);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new Lazy<T>(action);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Fields >
|
||||
|
||||
//These objects are the underlying Objects that may be bound to by consumers.
|
||||
//The values are updated upon request of the associated property.
|
||||
//This is so that properties are not returning new objects every request (which would break bindings)
|
||||
//If the statistic is never requested, then Lazy<> allows the list to skip performing the math against that statistic.
|
||||
|
||||
private Lazy<Statistic> Total_DirStatsField;
|
||||
private Lazy<Statistic> Total_ByteStatsField;
|
||||
private Lazy<Statistic> Total_FileStatsField;
|
||||
private Lazy<AverageSpeedStatistic> Average_SpeedStatsField;
|
||||
private Lazy<RoboCopyCombinedExitStatus> ExitStatusSummaryField;
|
||||
private readonly ObservableList<RoboCopyResults> ResultsList = new ObservableList<RoboCopyResults>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Events >
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for objects to send notification that the list behind an <see cref="IRoboCopyResultsList"/> interface has been updated
|
||||
/// </summary>
|
||||
public delegate void ResultsListUpdated(object sender, ResultListUpdatedEventArgs e);
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Public Properties >
|
||||
|
||||
/// <summary> Sum of all DirectoryStatistics objects </summary>
|
||||
/// <remarks>Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. </remarks>
|
||||
public IStatistic DirectoriesStatistic => Total_DirStatsField?.Value;
|
||||
|
||||
/// <summary> Sum of all ByteStatistics objects </summary>
|
||||
/// <remarks>Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. </remarks>
|
||||
public IStatistic BytesStatistic => Total_ByteStatsField?.Value;
|
||||
|
||||
/// <summary> Sum of all FileStatistics objects </summary>
|
||||
/// <remarks>Underlying value is Lazy{Statistic} object - Initial value not calculated until first request. </remarks>
|
||||
public IStatistic FilesStatistic => Total_FileStatsField?.Value;
|
||||
|
||||
/// <summary> Average of all SpeedStatistics objects </summary>
|
||||
/// <remarks>Underlying value is Lazy{SpeedStatistic} object - Initial value not calculated until first request. </remarks>
|
||||
public ISpeedStatistic SpeedStatistic => Average_SpeedStatsField?.Value;
|
||||
|
||||
/// <summary> Sum of all RoboCopyExitStatus objects </summary>
|
||||
/// <remarks>Underlying value is Lazy object - Initial value not calculated until first request. </remarks>
|
||||
public IRoboCopyCombinedExitStatus Status => ExitStatusSummaryField?.Value;
|
||||
|
||||
/// <summary> The Collection of RoboCopy Results. Add/Removal of <see cref="RoboCopyResults"/> objects must be performed through this object's methods, not on the list directly. </summary>
|
||||
public IReadOnlyList<RoboCopyResults> Collection => ResultsList;
|
||||
|
||||
/// <inheritdoc cref="List{T}.Count"/>
|
||||
public int Count => ResultsList.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Get or Set the element at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">The zero-based index of the item to Get or Set.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"/>
|
||||
public RoboCopyResults this[int index] { get => ResultsList[index]; set => ResultsList[index] = value; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Get Array Methods ( Public ) >
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of the ByteStatistics objects from this list.
|
||||
/// </summary>
|
||||
/// <returns>New array of the ByteStatistic objects</returns>
|
||||
public IStatistic[] GetByteStatistics()
|
||||
{
|
||||
List<Statistic> tmp = new List<Statistic> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.Add(r?.BytesStatistic);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of the DirectoriesStatistic objects from this list.
|
||||
/// </summary>
|
||||
/// <returns>New array of the DirectoriesStatistic objects</returns>
|
||||
public IStatistic[] GetDirectoriesStatistics()
|
||||
{
|
||||
List<Statistic> tmp = new List<Statistic> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.Add(r?.DirectoriesStatistic);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of the FilesStatistic objects from this list.
|
||||
/// </summary>
|
||||
/// <returns>New array of the FilesStatistic objects</returns>
|
||||
public IStatistic[] GetFilesStatistics()
|
||||
{
|
||||
List<Statistic> tmp = new List<Statistic> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.Add(r?.FilesStatistic);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of the FilesStatistic objects from this list.
|
||||
/// </summary>
|
||||
/// <returns>New array of the FilesStatistic objects</returns>
|
||||
public RoboCopyExitStatus[] GetStatuses()
|
||||
{
|
||||
List<RoboCopyExitStatus> tmp = new List<RoboCopyExitStatus> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.Add(r?.Status);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of the FilesStatistic objects from this list.
|
||||
/// </summary>
|
||||
/// <returns>New array of the FilesStatistic objects</returns>
|
||||
public ISpeedStatistic[] GetSpeedStatistics()
|
||||
{
|
||||
List<SpeedStatistic> tmp = new List<SpeedStatistic> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.Add(r?.SpeedStatistic);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the <see cref="RoboCopyResults.RoboCopyErrors"/> into a single array of errors
|
||||
/// </summary>
|
||||
/// <returns>New array of the ErrorEventArgs objects</returns>
|
||||
public ErrorEventArgs[] GetErrors()
|
||||
{
|
||||
List<ErrorEventArgs> tmp = new List<ErrorEventArgs> { };
|
||||
foreach (RoboCopyResults r in this)
|
||||
tmp.AddRange(r?.RoboCopyErrors);
|
||||
return tmp.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < INotifyCollectionChanged >
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ObservableList{T}.CollectionChanged"/>
|
||||
/// </summary>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
/// <summary>Process the Added/Removed items, then fire the event</summary>
|
||||
/// <inheritdoc cref="ObservableList{T}.OnCollectionChanged(NotifyCollectionChangedEventArgs)"/>
|
||||
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.Action == NotifyCollectionChangedAction.Move) goto RaiseEvent; // Sorting causes no change in math -> Simply raise the event
|
||||
|
||||
//Reset means a drastic change -> Recalculate everything, then goto RaiseEvent
|
||||
if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||
{
|
||||
//Bytes
|
||||
if (Total_ByteStatsField.IsValueCreated)
|
||||
{
|
||||
Total_ByteStatsField.Value.Reset(false);
|
||||
Total_ByteStatsField.Value.AddStatistic(GetByteStatistics());
|
||||
}
|
||||
//Directories
|
||||
if (Total_DirStatsField.IsValueCreated)
|
||||
{
|
||||
Total_DirStatsField.Value.Reset(false);
|
||||
Total_DirStatsField.Value.AddStatistic(GetDirectoriesStatistics());
|
||||
}
|
||||
//Files
|
||||
if (Total_FileStatsField.IsValueCreated)
|
||||
{
|
||||
Total_FileStatsField.Value.Reset(false);
|
||||
Total_FileStatsField.Value.AddStatistic(GetFilesStatistics());
|
||||
}
|
||||
//Exit Status
|
||||
if (ExitStatusSummaryField.IsValueCreated)
|
||||
{
|
||||
ExitStatusSummaryField.Value.Reset(false);
|
||||
ExitStatusSummaryField.Value.CombineStatus(GetStatuses());
|
||||
}
|
||||
//Speed
|
||||
if (Average_SpeedStatsField.IsValueCreated)
|
||||
{
|
||||
Average_SpeedStatsField.Value.Reset(false);
|
||||
Average_SpeedStatsField.Value.Average(GetSpeedStatistics());
|
||||
}
|
||||
|
||||
goto RaiseEvent;
|
||||
}
|
||||
|
||||
//Process New Items
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
int i = 0;
|
||||
int i2 = e.NewItems.Count;
|
||||
foreach (RoboCopyResults r in e?.NewItems)
|
||||
{
|
||||
i++;
|
||||
bool RaiseValueChangeEvent = (e.OldItems == null || e.OldItems.Count == 0) && (i == i2); //Prevent raising the event if more calculation needs to be performed either from NewItems or from OldItems
|
||||
//Bytes
|
||||
if (Total_ByteStatsField.IsValueCreated)
|
||||
Total_ByteStatsField.Value.AddStatistic(r?.BytesStatistic, RaiseValueChangeEvent);
|
||||
//Directories
|
||||
if (Total_DirStatsField.IsValueCreated)
|
||||
Total_DirStatsField.Value.AddStatistic(r?.DirectoriesStatistic, RaiseValueChangeEvent);
|
||||
//Files
|
||||
if (Total_FileStatsField.IsValueCreated)
|
||||
Total_FileStatsField.Value.AddStatistic(r?.FilesStatistic, RaiseValueChangeEvent);
|
||||
//Exit Status
|
||||
if (ExitStatusSummaryField.IsValueCreated)
|
||||
ExitStatusSummaryField.Value.CombineStatus(r?.Status, RaiseValueChangeEvent);
|
||||
//Speed
|
||||
if (Average_SpeedStatsField.IsValueCreated)
|
||||
{
|
||||
Average_SpeedStatsField.Value.Add(r?.SpeedStatistic);
|
||||
if (RaiseValueChangeEvent) Average_SpeedStatsField.Value.CalculateAverage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Process Removed Items
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
int i = 0;
|
||||
int i2 = e.OldItems.Count;
|
||||
foreach (RoboCopyResults r in e?.OldItems)
|
||||
{
|
||||
i++;
|
||||
bool RaiseValueChangeEvent = i == i2;
|
||||
//Bytes
|
||||
if (Total_ByteStatsField.IsValueCreated)
|
||||
Total_ByteStatsField.Value.Subtract(r?.BytesStatistic, RaiseValueChangeEvent);
|
||||
//Directories
|
||||
if (Total_DirStatsField.IsValueCreated)
|
||||
Total_DirStatsField.Value.Subtract(r?.DirectoriesStatistic, RaiseValueChangeEvent);
|
||||
//Files
|
||||
if (Total_FileStatsField.IsValueCreated)
|
||||
Total_FileStatsField.Value.Subtract(r?.FilesStatistic, RaiseValueChangeEvent);
|
||||
//Exit Status
|
||||
if (ExitStatusSummaryField.IsValueCreated && RaiseValueChangeEvent)
|
||||
{
|
||||
ExitStatusSummaryField.Value.Reset(false);
|
||||
ExitStatusSummaryField.Value.CombineStatus(GetStatuses());
|
||||
}
|
||||
//Speed
|
||||
if (Average_SpeedStatsField.IsValueCreated)
|
||||
{
|
||||
if (this.Count == 0)
|
||||
Average_SpeedStatsField.Value.Reset();
|
||||
else
|
||||
Average_SpeedStatsField.Value.Subtract(r.SpeedStatistic);
|
||||
if (RaiseValueChangeEvent) Average_SpeedStatsField.Value.CalculateAverage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RaiseEvent:
|
||||
//Raise the CollectionChanged event
|
||||
CollectionChanged?.Invoke(this, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < ICloneable >
|
||||
|
||||
/// <summary> Clone this object to a new RoboCopyResultsList </summary>
|
||||
public RoboCopyResultsList Clone() => new RoboCopyResultsList(this);
|
||||
|
||||
#endregion
|
||||
|
||||
#region < IList{T} Implementation >
|
||||
|
||||
bool ICollection<RoboCopyResults>.IsReadOnly => false;
|
||||
|
||||
/// <inheritdoc cref="IList{T}.IndexOf(T)"/>
|
||||
public int IndexOf(RoboCopyResults item) => ResultsList.IndexOf(item);
|
||||
|
||||
/// <inheritdoc cref="ObservableList{T}.Insert(int, T)"/>
|
||||
public void Insert(int index, RoboCopyResults item) => ResultsList.Insert(index, item);
|
||||
|
||||
/// <inheritdoc cref="ObservableList{T}.RemoveAt(int)"/>
|
||||
public void RemoveAt(int index) => ResultsList.RemoveAt(index);
|
||||
|
||||
/// <inheritdoc cref="ObservableList{T}.Add(T)"/>
|
||||
public void Add(RoboCopyResults item) => ResultsList.Add(item);
|
||||
|
||||
/// <inheritdoc cref="ObservableList{T}.Clear"/>
|
||||
public void Clear() => ResultsList.Clear();
|
||||
|
||||
/// <inheritdoc cref="IList.Contains(object)"/>
|
||||
public bool Contains(RoboCopyResults item) => ResultsList.Contains(item);
|
||||
|
||||
/// <inheritdoc cref="ICollection.CopyTo(Array, int)"/>
|
||||
public void CopyTo(RoboCopyResults[] array, int arrayIndex) => ResultsList.CopyTo(array, arrayIndex);
|
||||
|
||||
/// <inheritdoc cref="ObservableList{T}.Remove(T)"/>
|
||||
public bool Remove(RoboCopyResults item) => ResultsList.Remove(item);
|
||||
|
||||
/// <inheritdoc cref="List{T}.GetEnumerator"/>
|
||||
public IEnumerator<RoboCopyResults> GetEnumerator() => ResultsList.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => ResultsList.GetEnumerator();
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
245
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsStatus.cs
Normal file
245
FSI.Lib/Tools/RoboSharp/Results/RoboCopyResultsStatus.cs
Normal file
@@ -0,0 +1,245 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Object that evaluates the ExitCode reported after RoboCopy finishes executing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/RoboCopyExitStatus"/>
|
||||
/// </remarks>
|
||||
public class RoboCopyExitStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:System.Object"/> class.
|
||||
/// </summary>
|
||||
public RoboCopyExitStatus(int exitCodeValue)
|
||||
{
|
||||
ExitCodeValue = exitCodeValue;
|
||||
}
|
||||
|
||||
/// <summary>ExitCode as reported by RoboCopy</summary>
|
||||
public int ExitCodeValue { get; protected set; }
|
||||
|
||||
/// <summary>ExitCode reported by RoboCopy converted into the Enum</summary>
|
||||
public RoboCopyExitCodes ExitCode => (RoboCopyExitCodes)ExitCodeValue;
|
||||
|
||||
/// <inheritdoc cref="RoboCopyExitCodes.FilesCopiedSuccessful"/>
|
||||
public bool Successful => !WasCancelled && ExitCodeValue < 0x10;
|
||||
|
||||
/// <inheritdoc cref="RoboCopyExitCodes.MismatchedDirectoriesDetected"/>
|
||||
public bool HasWarnings => ExitCodeValue >= 0x4;
|
||||
|
||||
/// <inheritdoc cref="RoboCopyExitCodes.SeriousErrorOccurred"/>
|
||||
public bool HasErrors => ExitCodeValue >= 0x10;
|
||||
|
||||
/// <inheritdoc cref="RoboCopyExitCodes.Cancelled"/>
|
||||
public virtual bool WasCancelled => ExitCodeValue < 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current object.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"ExitCode: {ExitCodeValue} ({ExitCode})";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the combination of multiple Exit Statuses
|
||||
/// </summary>
|
||||
public sealed class RoboCopyCombinedExitStatus : RoboCopyExitStatus, IRoboCopyCombinedExitStatus
|
||||
{
|
||||
#region < Constructor >
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoboCopyCombinedExitStatus"/> class.
|
||||
/// </summary>
|
||||
public RoboCopyCombinedExitStatus() : base(0) { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoboCopyCombinedExitStatus"/> class.
|
||||
/// </summary>
|
||||
public RoboCopyCombinedExitStatus(int exitCodeValue) : base(exitCodeValue) { }
|
||||
|
||||
/// <summary>
|
||||
/// Clone this into a new instance
|
||||
/// </summary>
|
||||
/// <param name="obj"></param>
|
||||
public RoboCopyCombinedExitStatus(RoboCopyCombinedExitStatus obj) :base((int)obj.ExitCode)
|
||||
{
|
||||
wascancelled = obj.wascancelled;
|
||||
noCopyNoError = obj.noCopyNoError;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Fields and Event >
|
||||
|
||||
//Private bools for the Combine methods
|
||||
private bool wascancelled;
|
||||
private bool noCopyNoError;
|
||||
private bool EnablePropertyChangeEvent = true;
|
||||
|
||||
/// <summary>This event when the ExitStatus summary has changed </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Public Properties >
|
||||
|
||||
/// <summary>Overides <see cref="RoboCopyExitStatus.WasCancelled"/></summary>
|
||||
/// <returns> <see cref="AnyWasCancelled"/></returns>
|
||||
public override bool WasCancelled => AnyWasCancelled;
|
||||
|
||||
/// <summary>
|
||||
/// Atleast one <see cref="RoboCopyExitStatus"/> objects combined into this result resulted in no errors and no files/directories copied.
|
||||
/// </summary>
|
||||
public bool AnyNoCopyNoError => noCopyNoError || ExitCodeValue == 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// Atleast one <see cref="RoboCopyExitStatus"/> object combined into this result had been cancelled / exited prior to completion.
|
||||
/// </summary>
|
||||
public bool AnyWasCancelled => wascancelled || ExitCodeValue < 0x0;
|
||||
|
||||
/// <summary>
|
||||
/// All jobs completed without errors or warnings.
|
||||
/// </summary>
|
||||
public bool AllSuccessful => !WasCancelled && (ExitCodeValue == 0x0 || ExitCodeValue == 0x1);
|
||||
|
||||
/// <summary>
|
||||
/// All jobs completed without errors or warnings, but Extra Files/Folders were detected.
|
||||
/// </summary>
|
||||
public bool AllSuccessful_WithWarnings => !WasCancelled && Successful && ExitCodeValue > 0x1;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < RaiseEvent Methods >
|
||||
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
private object[] StoreCurrentValues()
|
||||
{
|
||||
return new object[10]
|
||||
{
|
||||
WasCancelled, AnyNoCopyNoError, AnyWasCancelled, AllSuccessful, AllSuccessful_WithWarnings, HasErrors, HasWarnings, Successful, ExitCode, ExitCodeValue
|
||||
};
|
||||
}
|
||||
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
private void CompareAndRaiseEvents(object[] OriginalValues)
|
||||
{
|
||||
object[] CurrentValues = StoreCurrentValues();
|
||||
if (CurrentValues[0] != OriginalValues[0]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("WasCancelled"));
|
||||
if (CurrentValues[1] != OriginalValues[1]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AnyNoCopyNoError"));
|
||||
if (CurrentValues[2] != OriginalValues[2]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AnyWasCancelled"));
|
||||
if (CurrentValues[3] != OriginalValues[3]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AllSuccessful"));
|
||||
if (CurrentValues[4] != OriginalValues[4]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("AllSuccessful_WithWarnings"));
|
||||
if (CurrentValues[5] != OriginalValues[5]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasErrors"));
|
||||
if (CurrentValues[6] != OriginalValues[6]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("HasWarnings"));
|
||||
if (CurrentValues[7] != OriginalValues[7]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Successful"));
|
||||
if (CurrentValues[8] != OriginalValues[8]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ExitCode"));
|
||||
if (CurrentValues[9] != OriginalValues[9]) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ExitCodeValue"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Combine the RoboCopyExitCodes of the supplied ExitStatus with this ExitStatus.
|
||||
/// </summary>
|
||||
/// <remarks>If any were Cancelled, set the WasCancelled property to TRUE. Otherwise combine the exit codes.</remarks>
|
||||
/// <param name="status">ExitStatus to combine with</param>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
public void CombineStatus(RoboCopyExitStatus status)
|
||||
{
|
||||
if (status == null) return;
|
||||
object[] OriginalValues = EnablePropertyChangeEvent ? StoreCurrentValues() : null;
|
||||
//Combine the status
|
||||
if (status.WasCancelled)
|
||||
{
|
||||
wascancelled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (status.ExitCode == 0x0) this.noCopyNoError = true; //0x0 is lost if any other values have been added, so this logs the state
|
||||
RoboCopyExitCodes code = this.ExitCode | status.ExitCode;
|
||||
this.ExitCodeValue = (int)code;
|
||||
}
|
||||
//Raise Property Change Events
|
||||
if (EnablePropertyChangeEvent) CompareAndRaiseEvents(OriginalValues);
|
||||
}
|
||||
|
||||
internal void CombineStatus(RoboCopyExitStatus status, bool enablePropertyChangeEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangeEvent;
|
||||
CombineStatus(status);
|
||||
EnablePropertyChangeEvent = enablePropertyChangeEvent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine all the RoboCopyExitStatuses together.
|
||||
/// </summary>
|
||||
/// <param name="status">Array or List of ExitStatuses to combine.</param>
|
||||
public void CombineStatus(IEnumerable<RoboCopyExitStatus> status)
|
||||
{
|
||||
foreach (RoboCopyExitStatus s in status)
|
||||
{
|
||||
EnablePropertyChangeEvent = s == status.Last();
|
||||
CombineStatus(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine all the RoboCopyExitStatuses together.
|
||||
/// </summary>
|
||||
/// <param name="statuses">Array or List of ExitStatuses to combine.</param>
|
||||
/// <returns> new RoboCopyExitStatus object </returns>
|
||||
public static RoboCopyCombinedExitStatus CombineStatuses(IEnumerable<RoboCopyExitStatus> statuses)
|
||||
{
|
||||
RoboCopyCombinedExitStatus ret = new RoboCopyCombinedExitStatus(0);
|
||||
ret.CombineStatus(statuses);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the value of the object
|
||||
/// </summary>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
public void Reset()
|
||||
{
|
||||
object[] OriginalValues = EnablePropertyChangeEvent ? StoreCurrentValues() : null;
|
||||
this.wascancelled = false;
|
||||
this.noCopyNoError = false;
|
||||
this.ExitCodeValue = 0;
|
||||
if (EnablePropertyChangeEvent) CompareAndRaiseEvents(OriginalValues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the value of the object
|
||||
/// </summary>
|
||||
internal void Reset(bool enablePropertyChangeEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangeEvent;
|
||||
Reset();
|
||||
EnablePropertyChangeEvent = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public RoboCopyCombinedExitStatus Clone() => new RoboCopyCombinedExitStatus(this);
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
}
|
||||
}
|
||||
265
FSI.Lib/Tools/RoboSharp/Results/RoboQueueProgressEstimator.cs
Normal file
265
FSI.Lib/Tools/RoboSharp/Results/RoboQueueProgressEstimator.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
using FSI.Lib.Tools.RoboSharp.EventArgObjects;
|
||||
using FSI.Lib.Tools.RoboSharp.Results;
|
||||
using WhereToAdd = FSI.Lib.Tools.RoboSharp.Results.ProgressEstimator.WhereToAdd;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Updates the Statistics every 250ms
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/RoboQueueProgressEstimator"/>
|
||||
/// </remarks>
|
||||
internal class RoboQueueProgressEstimator : IProgressEstimator, IResults, IDisposable
|
||||
{
|
||||
#region < Constructors >
|
||||
|
||||
internal RoboQueueProgressEstimator()
|
||||
{
|
||||
tmpDirs = new Statistic(Statistic.StatType.Directories);
|
||||
tmpFiles = new Statistic(Statistic.StatType.Files);
|
||||
tmpBytes = new Statistic(Statistic.StatType.Bytes);
|
||||
|
||||
tmpDirs.EnablePropertyChangeEvent = false;
|
||||
tmpFiles.EnablePropertyChangeEvent = false;
|
||||
tmpBytes.EnablePropertyChangeEvent = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Private Members >
|
||||
|
||||
//ThreadSafe Bags/Queues
|
||||
private readonly ConcurrentDictionary<IProgressEstimator, object> SubscribedStats = new ConcurrentDictionary<IProgressEstimator, object>();
|
||||
|
||||
//Stats
|
||||
private readonly Statistic DirStatField = new Statistic(Statistic.StatType.Directories, "Directory Stats Estimate");
|
||||
private readonly Statistic FileStatsField = new Statistic(Statistic.StatType.Files, "File Stats Estimate");
|
||||
private readonly Statistic ByteStatsField = new Statistic(Statistic.StatType.Bytes, "Byte Stats Estimate");
|
||||
|
||||
//Add Tasks
|
||||
private int UpdatePeriodInMilliSecond = 250;
|
||||
private readonly Statistic tmpDirs;
|
||||
private readonly Statistic tmpFiles;
|
||||
private readonly Statistic tmpBytes;
|
||||
private DateTime NextUpdate = DateTime.Now;
|
||||
private readonly object StatLock = new object();
|
||||
private readonly object UpdateLock = new object();
|
||||
private bool disposedValue;
|
||||
|
||||
#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());
|
||||
|
||||
/// <inheritdoc cref="IProgressEstimator.ValuesUpdated"/>
|
||||
public event ProgressEstimator.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 (FilesStatistic.Copied > 0)
|
||||
code |= Results.RoboCopyExitCodes.FilesCopiedSuccessful;
|
||||
|
||||
//Extra
|
||||
if (DirectoriesStatistic.Extras > 0 || FilesStatistic.Extras > 0)
|
||||
code |= Results.RoboCopyExitCodes.ExtraFilesOrDirectoriesDetected;
|
||||
|
||||
//MisMatch
|
||||
if (DirectoriesStatistic.Mismatch > 0 || FilesStatistic.Mismatch > 0)
|
||||
code |= Results.RoboCopyExitCodes.MismatchedDirectoriesDetected;
|
||||
|
||||
//Failed
|
||||
if (DirectoriesStatistic.Failed > 0 || FilesStatistic.Failed > 0)
|
||||
code |= Results.RoboCopyExitCodes.SomeFilesOrDirectoriesCouldNotBeCopied;
|
||||
|
||||
return code;
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Counting Methods ( private ) >
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to the update events of a <see cref="ProgressEstimator"/> object
|
||||
/// </summary>
|
||||
internal void BindToProgressEstimator(IProgressEstimator estimator)
|
||||
{
|
||||
if (!SubscribedStats.ContainsKey(estimator))
|
||||
{
|
||||
SubscribedStats.TryAdd(estimator, null);
|
||||
estimator.ValuesUpdated += Estimator_ValuesUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private void Estimator_ValuesUpdated(IProgressEstimator sender, IProgressEstimatorUpdateEventArgs e)
|
||||
{
|
||||
Statistic bytes = null;
|
||||
Statistic files = null;
|
||||
Statistic dirs = null;
|
||||
bool updateReqd = false;
|
||||
|
||||
//Wait indefinitely until the tmp stats are released -- Previously -> lock(StatLock) { }
|
||||
Monitor.Enter(StatLock);
|
||||
|
||||
//Update the Temp Stats
|
||||
tmpBytes.AddStatistic(e.ValueChange_Bytes);
|
||||
tmpFiles.AddStatistic(e.ValueChange_Files);
|
||||
tmpDirs.AddStatistic(e.ValueChange_Directories);
|
||||
|
||||
//Check if an update should be pushed to the public fields
|
||||
//If another thread already has this locked, then it is pushing out an update -> This thread exits the method after releasing StatLock
|
||||
if (Monitor.TryEnter(UpdateLock))
|
||||
{
|
||||
updateReqd = DateTime.Now >= NextUpdate && (tmpFiles.NonZeroValue || tmpDirs.NonZeroValue || tmpBytes.NonZeroValue);
|
||||
if (updateReqd)
|
||||
{
|
||||
if (tmpBytes.NonZeroValue)
|
||||
{
|
||||
bytes = tmpBytes.Clone();
|
||||
tmpBytes.Reset();
|
||||
}
|
||||
|
||||
if (tmpFiles.NonZeroValue)
|
||||
{
|
||||
files = tmpFiles.Clone();
|
||||
tmpFiles.Reset();
|
||||
}
|
||||
|
||||
if (tmpDirs.NonZeroValue)
|
||||
{
|
||||
dirs = tmpDirs.Clone();
|
||||
tmpDirs.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Monitor.Exit(StatLock); //Release the tmp Stats lock
|
||||
|
||||
if (updateReqd)
|
||||
{
|
||||
// Perform the Add Events
|
||||
if (bytes != null) ByteStatsField.AddStatistic(bytes);
|
||||
if (files != null) FileStatsField.AddStatistic(files);
|
||||
if (dirs != null) DirStatField.AddStatistic(dirs);
|
||||
//Raise the event, then update the NextUpdate time period
|
||||
ValuesUpdated?.Invoke(this, new IProgressEstimatorUpdateEventArgs(this, bytes, files, dirs));
|
||||
NextUpdate = DateTime.Now.AddMilliseconds(UpdatePeriodInMilliSecond);
|
||||
}
|
||||
|
||||
//Release the UpdateLock
|
||||
if (Monitor.IsEntered(UpdateLock))
|
||||
Monitor.Exit(UpdateLock);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from all bound Statistic objects
|
||||
/// </summary>
|
||||
internal void UnBind()
|
||||
{
|
||||
if (SubscribedStats != null)
|
||||
{
|
||||
foreach (var est in SubscribedStats.Keys)
|
||||
{
|
||||
est.ValuesUpdated -= Estimator_ValuesUpdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < CancelTasks & DisposePattern >
|
||||
|
||||
/// <summary>
|
||||
/// Unbind all the ProgressEstimators
|
||||
/// </summary>
|
||||
internal void CancelTasks() => CancelTasks(true);
|
||||
|
||||
private void CancelTasks(bool RunUpdateTask)
|
||||
{
|
||||
//Preventn any additional events coming through
|
||||
UnBind();
|
||||
//Push the last update out after a short delay to allow any pending events through
|
||||
if (RunUpdateTask)
|
||||
{
|
||||
Task.Run( async () => {
|
||||
lock (UpdateLock)
|
||||
NextUpdate = DateTime.Now.AddMilliseconds(124);
|
||||
await Task.Delay(125);
|
||||
Estimator_ValuesUpdated(null, IProgressEstimatorUpdateEventArgs.DummyArgs);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// TODO: dispose managed state (managed objects)
|
||||
}
|
||||
|
||||
//Cancel the tasks
|
||||
CancelTasks(false);
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||
~RoboQueueProgressEstimator()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
151
FSI.Lib/Tools/RoboSharp/Results/RoboQueueResults.cs
Normal file
151
FSI.Lib/Tools/RoboSharp/Results/RoboQueueResults.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Object returned by RoboQueue when a run has completed.
|
||||
/// </summary>
|
||||
public sealed class RoboQueueResults : IRoboQueueResults, IRoboCopyResultsList, ITimeSpan
|
||||
{
|
||||
internal RoboQueueResults()
|
||||
{
|
||||
collection = new RoboCopyResultsList();
|
||||
StartTime = DateTime.Now;
|
||||
QueueProcessRunning = true;
|
||||
}
|
||||
|
||||
private RoboCopyResultsList collection { get; }
|
||||
private DateTime EndTimeField;
|
||||
private TimeSpan TimeSpanField;
|
||||
private bool QueueProcessRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Add a result to the collection
|
||||
/// </summary>
|
||||
internal void Add(RoboCopyResults result) => collection.Add(result);
|
||||
|
||||
#region < IRoboQueueResults >
|
||||
|
||||
/// <summary> Time the RoboQueue task was started </summary>
|
||||
public DateTime StartTime { get; }
|
||||
|
||||
/// <summary> Time the RoboQueue task was completed / cancelled. </summary>
|
||||
/// <remarks> Should Only considered valid if <see cref="QueueComplete"/> = true.</remarks>
|
||||
public DateTime EndTime
|
||||
{
|
||||
get => EndTimeField;
|
||||
internal set
|
||||
{
|
||||
EndTimeField = value;
|
||||
TimeSpanField = value.Subtract(StartTime);
|
||||
QueueProcessRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Length of Time RoboQueue was running </summary>
|
||||
/// <remarks> Should Only considered valid if <see cref="QueueComplete"/> = true.</remarks>
|
||||
public TimeSpan TimeSpan => TimeSpanField;
|
||||
|
||||
/// <summary> TRUE if the RoboQueue object that created this results set has not finished running yet. </summary>
|
||||
public bool QueueRunning => QueueProcessRunning;
|
||||
|
||||
/// <summary> TRUE if the RoboQueue object that created this results has completed running, or has been cancelled. </summary>
|
||||
public bool QueueComplete => !QueueProcessRunning;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < IRoboCopyResultsList Implementation >
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.DirectoriesStatistic"/>
|
||||
public IStatistic DirectoriesStatistic => ((IRoboCopyResultsList)collection).DirectoriesStatistic;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.BytesStatistic"/>
|
||||
public IStatistic BytesStatistic => ((IRoboCopyResultsList)collection).BytesStatistic;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.FilesStatistic"/>
|
||||
public IStatistic FilesStatistic => ((IRoboCopyResultsList)collection).FilesStatistic;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.SpeedStatistic"/>
|
||||
public ISpeedStatistic SpeedStatistic => ((IRoboCopyResultsList)collection).SpeedStatistic;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.Status"/>
|
||||
public IRoboCopyCombinedExitStatus Status => ((IRoboCopyResultsList)collection).Status;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.Collection"/>
|
||||
public IReadOnlyList<RoboCopyResults> Collection => ((IRoboCopyResultsList)collection).Collection;
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.Count"/>
|
||||
public int Count => ((IRoboCopyResultsList)collection).Count;
|
||||
|
||||
public RoboCopyResults this[int i] => ((IRoboCopyResultsList)collection)[i];
|
||||
|
||||
/// <inheritdoc cref="RoboCopyResultsList.CollectionChanged"/>
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add
|
||||
{
|
||||
((INotifyCollectionChanged)collection).CollectionChanged += value;
|
||||
}
|
||||
|
||||
remove
|
||||
{
|
||||
((INotifyCollectionChanged)collection).CollectionChanged -= value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetByteStatistics"/>
|
||||
public IStatistic[] GetByteStatistics()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetByteStatistics();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetDirectoriesStatistics"/>
|
||||
public IStatistic[] GetDirectoriesStatistics()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetDirectoriesStatistics();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="RoboCopyResultsList.GetEnumerator"/>
|
||||
public IEnumerator<RoboCopyResults> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<RoboCopyResults>)collection).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetFilesStatistics"/>
|
||||
public IStatistic[] GetFilesStatistics()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetFilesStatistics();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetSpeedStatistics"/>
|
||||
public ISpeedStatistic[] GetSpeedStatistics()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetSpeedStatistics();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetStatuses"/>
|
||||
public RoboCopyExitStatus[] GetStatuses()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetStatuses();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)collection).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRoboCopyResultsList.GetErrors"/>
|
||||
public ErrorEventArgs[] GetErrors()
|
||||
{
|
||||
return ((IRoboCopyResultsList)collection).GetErrors();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
387
FSI.Lib/Tools/RoboSharp/Results/SpeedStatistic.cs
Normal file
387
FSI.Lib/Tools/RoboSharp/Results/SpeedStatistic.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information regarding average Transfer Speed. <br/>
|
||||
/// Note: Runs that do not perform any copy operations or that exited prematurely ( <see cref="RoboCopyExitCodes.Cancelled"/> ) will result in a null <see cref="SpeedStatistic"/> object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/SpeedStatistic"/>
|
||||
/// </remarks>
|
||||
public class SpeedStatistic : INotifyPropertyChanged, ISpeedStatistic
|
||||
{
|
||||
/// <summary>
|
||||
/// Create new SpeedStatistic
|
||||
/// </summary>
|
||||
public SpeedStatistic() { }
|
||||
|
||||
/// <summary>
|
||||
/// Clone a SpeedStatistic
|
||||
/// </summary>
|
||||
public SpeedStatistic(SpeedStatistic stat)
|
||||
{
|
||||
BytesPerSec = stat.BytesPerSec;
|
||||
MegaBytesPerMin = stat.MegaBytesPerMin;
|
||||
}
|
||||
|
||||
#region < Private & Protected Members >
|
||||
|
||||
private decimal BytesPerSecField = 0;
|
||||
private decimal MegaBytesPerMinField = 0;
|
||||
|
||||
/// <summary> This toggle Enables/Disables firing the <see cref="PropertyChanged"/> Event to avoid firing it when doing multiple consecutive changes to the values </summary>
|
||||
protected bool EnablePropertyChangeEvent { get; set; } = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Public Properties & Events >
|
||||
|
||||
/// <summary>This event will fire when the value of the SpeedStatistic is updated </summary>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>Raise Property Change Event</summary>
|
||||
protected void OnPropertyChange(string PropertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
|
||||
|
||||
/// <inheritdoc cref="ISpeedStatistic.BytesPerSec"/>
|
||||
public virtual decimal BytesPerSec
|
||||
{
|
||||
get => BytesPerSecField;
|
||||
protected set
|
||||
{
|
||||
if (BytesPerSecField != value)
|
||||
{
|
||||
BytesPerSecField = value;
|
||||
if (EnablePropertyChangeEvent) OnPropertyChange("MegaBytesPerMin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISpeedStatistic.BytesPerSec"/>
|
||||
public virtual decimal MegaBytesPerMin
|
||||
{
|
||||
get => MegaBytesPerMinField;
|
||||
protected set
|
||||
{
|
||||
if (MegaBytesPerMinField != value)
|
||||
{
|
||||
MegaBytesPerMinField = value;
|
||||
if (EnablePropertyChangeEvent) OnPropertyChange("MegaBytesPerMin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current object.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Speed: {BytesPerSec} Bytes/sec{Environment.NewLine}Speed: {MegaBytesPerMin} MegaBytes/min";
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISpeedStatistic.Clone"/>
|
||||
public virtual SpeedStatistic Clone() => new SpeedStatistic(this);
|
||||
|
||||
object ICloneable.Clone() => Clone();
|
||||
|
||||
internal static SpeedStatistic Parse(string line1, string line2)
|
||||
{
|
||||
var res = new SpeedStatistic();
|
||||
|
||||
var pattern = new Regex(@"\d+([\.,]\d+)?");
|
||||
Match match;
|
||||
|
||||
match = pattern.Match(line1);
|
||||
if (match.Success)
|
||||
{
|
||||
res.BytesPerSec = Convert.ToDecimal(match.Value.Replace(',', '.'), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
match = pattern.Match(line2);
|
||||
if (match.Success)
|
||||
{
|
||||
res.MegaBytesPerMin = Convert.ToDecimal(match.Value.Replace(',', '.'), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This object represents the Average of several <see cref="SpeedStatistic"/> objects, and contains
|
||||
/// methods to facilitate that functionality.
|
||||
/// </summary>
|
||||
public sealed class AverageSpeedStatistic : SpeedStatistic
|
||||
{
|
||||
#region < Constructors >
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new <see cref="AverageSpeedStatistic"/> object with the default values.
|
||||
/// </summary>
|
||||
public AverageSpeedStatistic() : base() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new <see cref="AverageSpeedStatistic"/> object. <br/>
|
||||
/// Values will be set to the return values of <see cref="SpeedStatistic.BytesPerSec"/> and <see cref="SpeedStatistic.MegaBytesPerMin"/> <br/>
|
||||
/// </summary>
|
||||
/// <param name="speedStat">
|
||||
/// Either a <see cref="SpeedStatistic"/> or a <see cref="AverageSpeedStatistic"/> object. <br/>
|
||||
/// If a <see cref="AverageSpeedStatistic"/> is passed into this constructor, it wil be treated as the base <see cref="SpeedStatistic"/> instead.
|
||||
/// </param>
|
||||
public AverageSpeedStatistic(ISpeedStatistic speedStat) : base()
|
||||
{
|
||||
Divisor = 1;
|
||||
Combined_BytesPerSec = speedStat.BytesPerSec;
|
||||
Combined_MegaBytesPerMin = speedStat.MegaBytesPerMin;
|
||||
CalculateAverage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new <see cref="AverageSpeedStatistic"/> object using <see cref="AverageSpeedStatistic.Average(IEnumerable{ISpeedStatistic})"/>. <br/>
|
||||
/// </summary>
|
||||
/// <param name="speedStats"><inheritdoc cref="Average(IEnumerable{ISpeedStatistic})"/></param>
|
||||
/// <inheritdoc cref="Average(IEnumerable{ISpeedStatistic})"/>
|
||||
public AverageSpeedStatistic(IEnumerable<ISpeedStatistic> speedStats) : base()
|
||||
{
|
||||
Average(speedStats);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone an AverageSpeedStatistic
|
||||
/// </summary>
|
||||
public AverageSpeedStatistic(AverageSpeedStatistic stat) : base(stat)
|
||||
{
|
||||
Divisor = stat.Divisor;
|
||||
Combined_BytesPerSec = stat.BytesPerSec;
|
||||
Combined_MegaBytesPerMin = stat.MegaBytesPerMin;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Fields >
|
||||
|
||||
/// <summary> Sum of all <see cref="SpeedStatistic.BytesPerSec"/> </summary>
|
||||
private decimal Combined_BytesPerSec = 0;
|
||||
|
||||
/// <summary> Sum of all <see cref="SpeedStatistic.MegaBytesPerMin"/> </summary>
|
||||
private decimal Combined_MegaBytesPerMin = 0;
|
||||
|
||||
/// <summary> Total number of SpeedStats that were combined to produce the Combined_* values </summary>
|
||||
private long Divisor = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Public Methods >
|
||||
|
||||
/// <inheritdoc cref="ICloneable.Clone"/>
|
||||
public override SpeedStatistic Clone() => new AverageSpeedStatistic(this);
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Reset Value Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Set the values for this object to 0
|
||||
/// </summary>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
public void Reset()
|
||||
{
|
||||
Combined_BytesPerSec = 0;
|
||||
Combined_MegaBytesPerMin = 0;
|
||||
Divisor = 0;
|
||||
BytesPerSec = 0;
|
||||
MegaBytesPerMin = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the values for this object to 0
|
||||
/// </summary>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void Reset(bool enablePropertyChangeEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangeEvent;
|
||||
Reset();
|
||||
EnablePropertyChangeEvent = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Add / Subtract methods are internal to allow usage within the RoboCopyResultsList object.
|
||||
// The 'Average' Methods will first Add the statistics to the current one, then recalculate the average.
|
||||
// Subtraction is only used when an item is removed from a RoboCopyResultsList
|
||||
// As such, public consumers should typically not require the use of subtract methods
|
||||
|
||||
#region < ADD ( internal ) >
|
||||
|
||||
/// <summary>
|
||||
/// Add the results of the supplied SpeedStatistic objects to this object. <br/>
|
||||
/// Does not automatically recalculate the average, and triggers no events.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If any supplied Speedstat object is actually an <see cref="AverageSpeedStatistic"/> object, default functionality will combine the private fields
|
||||
/// used to calculate the average speed instead of using the publicly reported speeds. <br/>
|
||||
/// This ensures that combining the average of multiple <see cref="AverageSpeedStatistic"/> objects returns the correct value. <br/>
|
||||
/// Ex: One object with 2 runs and one with 3 runs will return the average of all 5 runs instead of the average of two averages.
|
||||
/// </remarks>
|
||||
/// <param name="stat">SpeedStatistic Item to add</param>
|
||||
/// <param name="ForceTreatAsSpeedStat">
|
||||
/// Setting this to TRUE will instead combine the calculated average of the <see cref="AverageSpeedStatistic"/>, treating it as a single <see cref="SpeedStatistic"/> object. <br/>
|
||||
/// Ignore the private fields, and instead use the calculated speeds)
|
||||
/// </param>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void Add(ISpeedStatistic stat, bool ForceTreatAsSpeedStat = false)
|
||||
{
|
||||
if (stat == null) return;
|
||||
bool IsAverageStat = !ForceTreatAsSpeedStat && stat.GetType() == typeof(AverageSpeedStatistic);
|
||||
AverageSpeedStatistic AvgStat = IsAverageStat ? (AverageSpeedStatistic)stat : null;
|
||||
Divisor += IsAverageStat ? AvgStat.Divisor : 1;
|
||||
Combined_BytesPerSec += IsAverageStat ? AvgStat.Combined_BytesPerSec : stat.BytesPerSec;
|
||||
Combined_MegaBytesPerMin += IsAverageStat ? AvgStat.Combined_MegaBytesPerMin : stat.MegaBytesPerMin;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add the supplied SpeedStatistic collection to this object.
|
||||
/// </summary>
|
||||
/// <param name="stats">SpeedStatistic collection to add</param>
|
||||
/// <param name="ForceTreatAsSpeedStat"><inheritdoc cref="Add(ISpeedStatistic, bool)"/></param>
|
||||
/// <inheritdoc cref="Add(ISpeedStatistic, bool)" path="/remarks"/>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void Add(IEnumerable<ISpeedStatistic> stats, bool ForceTreatAsSpeedStat = false)
|
||||
{
|
||||
foreach (SpeedStatistic stat in stats)
|
||||
Add(stat, ForceTreatAsSpeedStat);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Subtract ( internal ) >
|
||||
|
||||
/// <summary>
|
||||
/// Subtract the results of the supplied SpeedStatistic objects from this object.<br/>
|
||||
/// </summary>
|
||||
/// <param name="stat">Statistics Item to add</param>
|
||||
/// <param name="ForceTreatAsSpeedStat"><inheritdoc cref="Add(ISpeedStatistic, bool)"/></param>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void Subtract(SpeedStatistic stat, bool ForceTreatAsSpeedStat = false)
|
||||
{
|
||||
if (stat == null) return;
|
||||
bool IsAverageStat = !ForceTreatAsSpeedStat && stat.GetType() == typeof(AverageSpeedStatistic);
|
||||
AverageSpeedStatistic AvgStat = IsAverageStat ? (AverageSpeedStatistic)stat : null;
|
||||
Divisor -= IsAverageStat ? AvgStat.Divisor : 1;
|
||||
//Combine the values if Divisor is still valid
|
||||
if (Divisor >= 1)
|
||||
{
|
||||
Combined_BytesPerSec -= IsAverageStat ? AvgStat.Combined_BytesPerSec : stat.BytesPerSec;
|
||||
Combined_MegaBytesPerMin -= IsAverageStat ? AvgStat.Combined_MegaBytesPerMin : stat.MegaBytesPerMin;
|
||||
}
|
||||
//Cannot have negative speeds or divisors -> Reset all values
|
||||
if (Divisor < 1 || Combined_BytesPerSec < 0 || Combined_MegaBytesPerMin < 0)
|
||||
{
|
||||
Combined_BytesPerSec = 0;
|
||||
Combined_MegaBytesPerMin = 0;
|
||||
Divisor = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtract the supplied SpeedStatistic collection from this object.
|
||||
/// </summary>
|
||||
/// <param name="stats">SpeedStatistic collection to subtract</param>
|
||||
/// <param name="ForceTreatAsSpeedStat"><inheritdoc cref="Add(ISpeedStatistic, bool)"/></param>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void Subtract(IEnumerable<SpeedStatistic> stats, bool ForceTreatAsSpeedStat = false)
|
||||
{
|
||||
foreach (SpeedStatistic stat in stats)
|
||||
Subtract(stat, ForceTreatAsSpeedStat);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < AVERAGE ( public ) >
|
||||
|
||||
/// <summary>
|
||||
/// Immediately recalculate the BytesPerSec and MegaBytesPerMin values
|
||||
/// </summary>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
internal void CalculateAverage()
|
||||
{
|
||||
EnablePropertyChangeEvent = false;
|
||||
//BytesPerSec
|
||||
var i = Divisor < 1 ? 0 : Math.Round(Combined_BytesPerSec / Divisor, 3);
|
||||
bool TriggerBPS = BytesPerSec != i;
|
||||
BytesPerSec = i;
|
||||
//MegaBytes
|
||||
i = Divisor < 1 ? 0 : Math.Round(Combined_MegaBytesPerMin / Divisor, 3);
|
||||
bool TriggerMBPM = MegaBytesPerMin != i;
|
||||
MegaBytesPerMin = i;
|
||||
//Trigger Events
|
||||
EnablePropertyChangeEvent = true;
|
||||
if (TriggerBPS) OnPropertyChange("BytesPerSec");
|
||||
if (TriggerMBPM) OnPropertyChange("MegaBytesPerMin");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the supplied <see cref="SpeedStatistic"/> objects, then get the average.
|
||||
/// </summary>
|
||||
/// <param name="stat">Stats object</param>
|
||||
/// <inheritdoc cref="Add(ISpeedStatistic, bool)" path="/remarks"/>
|
||||
public void Average(ISpeedStatistic stat)
|
||||
{
|
||||
Add(stat);
|
||||
CalculateAverage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the supplied <see cref="SpeedStatistic"/> objects, then get the average.
|
||||
/// </summary>
|
||||
/// <param name="stats">Collection of <see cref="ISpeedStatistic"/> objects</param>
|
||||
/// <inheritdoc cref="Add(ISpeedStatistic, bool)" path="/remarks"/>
|
||||
public void Average(IEnumerable<ISpeedStatistic> stats)
|
||||
{
|
||||
Add(stats);
|
||||
CalculateAverage();
|
||||
}
|
||||
|
||||
/// <returns>New Statistics Object</returns>
|
||||
/// <inheritdoc cref=" Average(IEnumerable{ISpeedStatistic})"/>
|
||||
public static AverageSpeedStatistic GetAverage(IEnumerable<ISpeedStatistic> stats)
|
||||
{
|
||||
AverageSpeedStatistic stat = new AverageSpeedStatistic();
|
||||
stat.Average(stats);
|
||||
return stat;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
816
FSI.Lib/Tools/RoboSharp/Results/Statistic.cs
Normal file
816
FSI.Lib/Tools/RoboSharp/Results/Statistic.cs
Normal file
@@ -0,0 +1,816 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using FSI.Lib.Tools.RoboSharp.Interfaces;
|
||||
using FSI.Lib.Tools.RoboSharp.EventArgObjects;
|
||||
|
||||
namespace FSI.Lib.Tools.RoboSharp.Results
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about number of items Copied, Skipped, Failed, etc.
|
||||
/// <para/>
|
||||
/// <see cref="RoboCopyResults"/> will not typically raise any events, but this object is used for other items, such as <see cref="ProgressEstimator"/> and <see cref="RoboCopyResultsList"/> to present results whose values may update periodically.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see href="https://github.com/tjscience/RoboSharp/wiki/Statistic"/>
|
||||
/// </remarks>
|
||||
public class Statistic : IStatistic
|
||||
{
|
||||
internal static IStatistic Default_Bytes = new Statistic(type: StatType.Bytes);
|
||||
internal static IStatistic Default_Files = new Statistic(type: StatType.Files);
|
||||
internal static IStatistic Default_Dirs = new Statistic(type: StatType.Directories);
|
||||
|
||||
#region < Constructors >
|
||||
|
||||
/// <summary> Create a new Statistic object of <see cref="StatType"/> </summary>
|
||||
[Obsolete("Statistic Types require Initialization with a StatType")]
|
||||
private Statistic() { }
|
||||
|
||||
/// <summary> Create a new Statistic object </summary>
|
||||
public Statistic(StatType type) { Type = type; }
|
||||
|
||||
/// <summary> Create a new Statistic object </summary>
|
||||
public Statistic(StatType type, string name) { Type = type; Name = name; }
|
||||
|
||||
/// <summary> Clone an existing Statistic object</summary>
|
||||
public Statistic(Statistic stat)
|
||||
{
|
||||
Type = stat.Type;
|
||||
NameField = stat.Name;
|
||||
TotalField = stat.Total;
|
||||
CopiedField = stat.Copied;
|
||||
SkippedField = stat.Skipped;
|
||||
MismatchField = stat.Mismatch;
|
||||
FailedField = stat.Failed;
|
||||
ExtrasField = stat.Extras;
|
||||
}
|
||||
|
||||
/// <summary> Clone an existing Statistic object</summary>
|
||||
internal Statistic(StatType type, string name, long total, long copied, long skipped, long mismatch, long failed, long extras)
|
||||
{
|
||||
Type = type;
|
||||
NameField = name;
|
||||
TotalField = total;
|
||||
CopiedField = copied;
|
||||
SkippedField = skipped;
|
||||
MismatchField = mismatch;
|
||||
FailedField = failed;
|
||||
ExtrasField = extras;
|
||||
}
|
||||
|
||||
/// <summary> Describe the Type of Statistics Object </summary>
|
||||
public enum StatType
|
||||
{
|
||||
/// <summary> Statistics object represents count of Directories </summary>
|
||||
Directories,
|
||||
/// <summary> Statistics object represents count of Files </summary>
|
||||
Files,
|
||||
/// <summary> Statistics object represents a Size ( number of bytes )</summary>
|
||||
Bytes
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Fields >
|
||||
|
||||
private string NameField = "";
|
||||
private long TotalField = 0;
|
||||
private long CopiedField = 0;
|
||||
private long SkippedField = 0;
|
||||
private long MismatchField = 0;
|
||||
private long FailedField = 0;
|
||||
private long ExtrasField = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Events >
|
||||
|
||||
/// <summary> This toggle Enables/Disables firing the <see cref="PropertyChanged"/> Event to avoid firing it when doing multiple consecutive changes to the values </summary>
|
||||
internal bool EnablePropertyChangeEvent = true;
|
||||
private bool PropertyChangedListener => EnablePropertyChangeEvent && PropertyChanged != null;
|
||||
private bool Listener_TotalChanged => EnablePropertyChangeEvent && OnTotalChanged != null;
|
||||
private bool Listener_CopiedChanged => EnablePropertyChangeEvent && OnCopiedChanged != null;
|
||||
private bool Listener_SkippedChanged => EnablePropertyChangeEvent && OnSkippedChanged != null;
|
||||
private bool Listener_MismatchChanged => EnablePropertyChangeEvent && OnMisMatchChanged != null;
|
||||
private bool Listener_FailedChanged => EnablePropertyChangeEvent && OnFailedChanged != null;
|
||||
private bool Listener_ExtrasChanged => EnablePropertyChangeEvent && OnExtrasChanged != null;
|
||||
|
||||
/// <summary>
|
||||
/// This event will fire when the value of the statistic is updated via Adding / Subtracting methods. <br/>
|
||||
/// Provides <see cref="StatisticPropertyChangedEventArgs"/> object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Allows use with both binding to controls and <see cref="ProgressEstimator"/> binding. <br/>
|
||||
/// EventArgs can be passed into <see cref="AddStatistic(PropertyChangedEventArgs)"/> after casting.
|
||||
/// </remarks>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>Handles any value changes </summary>
|
||||
public delegate void StatChangedHandler(Statistic sender, StatChangedEventArg e);
|
||||
|
||||
/// <summary> Occurs when the <see cref="Total"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnTotalChanged;
|
||||
|
||||
/// <summary> Occurs when the <see cref="Copied"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnCopiedChanged;
|
||||
|
||||
/// <summary> Occurs when the <see cref="Skipped"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnSkippedChanged;
|
||||
|
||||
/// <summary> Occurs when the <see cref="Mismatch"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnMisMatchChanged;
|
||||
|
||||
/// <summary> Occurs when the <see cref="Failed"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnFailedChanged;
|
||||
|
||||
/// <summary> Occurs when the <see cref="Extras"/> Property is updated. </summary>
|
||||
public event StatChangedHandler OnExtrasChanged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Properties >
|
||||
|
||||
/// <summary>
|
||||
/// Checks all values and determines if any of them are != 0.
|
||||
/// </summary>
|
||||
public bool NonZeroValue => TotalField != 0 || CopiedField != 0 || SkippedField != 0 || MismatchField != 0 || FailedField != 0 || ExtrasField != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the Statistics Object
|
||||
/// </summary>
|
||||
public string Name
|
||||
{
|
||||
get => NameField;
|
||||
set
|
||||
{
|
||||
if (value != NameField)
|
||||
{
|
||||
NameField = value ?? "";
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="StatType"/>
|
||||
/// </summary>
|
||||
public StatType Type { get; }
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Total"/>
|
||||
public long Total {
|
||||
get => TotalField;
|
||||
internal set
|
||||
{
|
||||
if (TotalField != value)
|
||||
{
|
||||
var e = PrepEventArgs(TotalField, value, "Total");
|
||||
TotalField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnTotalChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Copied"/>
|
||||
public long Copied {
|
||||
get => CopiedField;
|
||||
internal set
|
||||
{
|
||||
if (CopiedField != value)
|
||||
{
|
||||
var e = PrepEventArgs(CopiedField, value, "Copied");
|
||||
CopiedField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnCopiedChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Skipped"/>
|
||||
public long Skipped {
|
||||
get => SkippedField;
|
||||
internal set
|
||||
{
|
||||
if (SkippedField != value)
|
||||
{
|
||||
var e = PrepEventArgs(SkippedField, value, "Skipped");
|
||||
SkippedField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnSkippedChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Mismatch"/>
|
||||
public long Mismatch {
|
||||
get => MismatchField;
|
||||
internal set
|
||||
{
|
||||
if (MismatchField != value)
|
||||
{
|
||||
var e = PrepEventArgs(MismatchField, value, "Mismatch");
|
||||
MismatchField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnMisMatchChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Failed"/>
|
||||
public long Failed {
|
||||
get => FailedField;
|
||||
internal set
|
||||
{
|
||||
if (FailedField != value)
|
||||
{
|
||||
var e = PrepEventArgs(FailedField, value, "Failed");
|
||||
FailedField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnFailedChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Extras"/>
|
||||
public long Extras {
|
||||
get => ExtrasField;
|
||||
internal set
|
||||
{
|
||||
if (ExtrasField != value)
|
||||
{
|
||||
var e = PrepEventArgs(ExtrasField, value, "Extras");
|
||||
ExtrasField = value;
|
||||
if (e != null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, e.Value.Item2);
|
||||
OnExtrasChanged?.Invoke(this, e.Value.Item1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < ToString Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string that represents the current object.
|
||||
/// </summary>
|
||||
public override string ToString() => ToString(false, true, ", ");
|
||||
|
||||
/// <summary>
|
||||
/// Customize the returned string
|
||||
/// </summary>
|
||||
/// <param name="IncludeType">Include string representation of <see cref="Type"/></param>
|
||||
/// <param name="IncludePrefix">Include "Total:" / "Copied:" / etc in the string to identify the values</param>
|
||||
/// <param name="Delimiter">Value Delimieter</param>
|
||||
/// <param name="DelimiterAfterType">
|
||||
/// Include the delimiter after the 'Type' - Only used if <paramref name="IncludeType"/> us also true. <br/>
|
||||
/// When <paramref name="IncludeType"/> is true, a space always exist after the type string. This would add delimiter instead of the space.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// TRUE, TRUE, "," --> $"{Type} Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" <para/>
|
||||
/// FALSE, TRUE, "," --> $"Total: {Total}, Copied: {Copied}, Skipped: {Skipped}, Mismatch: {Mismatch}, Failed: {Failed}, Extras: {Extras}" <para/>
|
||||
/// FALSE, FALSE, "," --> $"{Total}, {Copied}, {Skipped}, {Mismatch}, {Failed}, {Extras}"
|
||||
/// </returns>
|
||||
public string ToString(bool IncludeType, bool IncludePrefix, string Delimiter, bool DelimiterAfterType = false)
|
||||
{
|
||||
return $"{ToString_Type(IncludeType, DelimiterAfterType)}" + $"{(IncludeType && DelimiterAfterType ? Delimiter : "")}" +
|
||||
$"{ToString_Total(false, IncludePrefix)}{Delimiter}" +
|
||||
$"{ToString_Copied(false, IncludePrefix)}{Delimiter}" +
|
||||
$"{ToString_Skipped(false, IncludePrefix)}{Delimiter}" +
|
||||
$"{ToString_Mismatch(false, IncludePrefix)}{Delimiter}" +
|
||||
$"{ToString_Failed(false, IncludePrefix)}{Delimiter}" +
|
||||
$"{ToString_Extras(false, IncludePrefix)}";
|
||||
}
|
||||
|
||||
/// <summary> Get the <see cref="Type"/> as a string </summary>
|
||||
public string ToString_Type() => ToString_Type(true).Trim();
|
||||
private string ToString_Type(bool IncludeType, bool Trim = false) => IncludeType ? $"{Type}{(Trim? "" : " ")}" : "";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Total"/></summary>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc cref="ToString(bool, bool, string, bool)"/>
|
||||
public string ToString_Total(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix? "Total: " : "")}{Total}";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Copied"/></summary>
|
||||
/// <inheritdoc cref="ToString_Total"/>
|
||||
public string ToString_Copied(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Copied: " : "")}{Copied}";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Extras"/></summary>
|
||||
/// <inheritdoc cref="ToString_Total"/>
|
||||
public string ToString_Extras(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Extras: " : "")}{Extras}";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Failed"/></summary>
|
||||
/// <inheritdoc cref="ToString_Total"/>
|
||||
public string ToString_Failed(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Failed: " : "")}{Failed}";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Mismatch"/></summary>
|
||||
/// <inheritdoc cref="ToString_Total"/>
|
||||
public string ToString_Mismatch(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Mismatch: " : "")}{Mismatch}";
|
||||
|
||||
/// <summary>Get the string describing the <see cref="Skipped"/></summary>
|
||||
/// <inheritdoc cref="ToString_Total"/>
|
||||
public string ToString_Skipped(bool IncludeType = false, bool IncludePrefix = true) => $"{ToString_Type(IncludeType)}{(IncludePrefix ? "Skipped: " : "")}{Skipped}";
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Parsing Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string and for the tokens reported by RoboCopy
|
||||
/// </summary>
|
||||
/// <param name="type">Statistic Type to produce</param>
|
||||
/// <param name="line">LogLine produced by RoboCopy in Summary Section</param>
|
||||
/// <returns>New Statistic Object</returns>
|
||||
public static Statistic Parse(StatType type, string line)
|
||||
{
|
||||
var res = new Statistic(type);
|
||||
|
||||
var tokenNames = new[] { nameof(Total), nameof(Copied), nameof(Skipped), nameof(Mismatch), nameof(Failed), nameof(Extras) };
|
||||
var patternBuilder = new StringBuilder(@"^.*:");
|
||||
|
||||
foreach (var tokenName in tokenNames)
|
||||
{
|
||||
var tokenPattern = GetTokenPattern(tokenName);
|
||||
patternBuilder.Append(@"\s+").Append(tokenPattern);
|
||||
}
|
||||
|
||||
var pattern = patternBuilder.ToString();
|
||||
var match = Regex.Match(line, pattern);
|
||||
if (!match.Success)
|
||||
return res;
|
||||
|
||||
var props = res.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||
foreach (var tokenName in tokenNames)
|
||||
{
|
||||
var prop = props.FirstOrDefault(x => x.Name == tokenName);
|
||||
if (prop == null)
|
||||
continue;
|
||||
|
||||
var tokenString = match.Groups[tokenName].Value;
|
||||
var tokenValue = ParseTokenString(tokenString);
|
||||
prop.SetValue(res, tokenValue, null);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private static string GetTokenPattern(string tokenName)
|
||||
{
|
||||
return $@"(?<{tokenName}>[\d\.]+(\s\w)?)";
|
||||
}
|
||||
|
||||
private static long ParseTokenString(string tokenString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tokenString))
|
||||
return 0;
|
||||
|
||||
tokenString = tokenString.Trim();
|
||||
if (Regex.IsMatch(tokenString, @"^\d+$", RegexOptions.Compiled))
|
||||
return long.Parse(tokenString);
|
||||
|
||||
var match = Regex.Match(tokenString, @"(?<Mains>[\d\.,]+)(\.(?<Fraction>\d+))\s(?<Unit>\w)", RegexOptions.Compiled);
|
||||
if (match.Success)
|
||||
{
|
||||
var mains = match.Groups["Mains"].Value.Replace(".", "").Replace(",", "");
|
||||
var fraction = match.Groups["Fraction"].Value;
|
||||
var unit = match.Groups["Unit"].Value.ToLower();
|
||||
|
||||
var number = double.Parse($"{mains}.{fraction}", NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
|
||||
switch (unit)
|
||||
{
|
||||
case "k":
|
||||
// Byte = kBytes * 1024
|
||||
number *= Math.Pow(1024, 1);
|
||||
break;
|
||||
case "m":
|
||||
// Byte = MBytes * 1024 * 1024
|
||||
number *= Math.Pow(1024, 2);
|
||||
break;
|
||||
case "g":
|
||||
// Byte = GBytes * 1024 * 1024 * 1024
|
||||
number *= Math.Pow(1024, 3);
|
||||
break;
|
||||
case "t":
|
||||
// Byte = TBytes * 1024 * 1024 * 1024 * 1024
|
||||
number *= Math.Pow(1024, 4);
|
||||
break;
|
||||
}
|
||||
|
||||
return Convert.ToInt64(number);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Reset Method >
|
||||
|
||||
/// <summary>
|
||||
/// Set the values for this object to 0
|
||||
/// </summary>
|
||||
public void Reset(bool enablePropertyChangeEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangeEvent;
|
||||
Reset();
|
||||
EnablePropertyChangeEvent = true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reset all values to Zero ( 0 ) -- Used by <see cref="RoboCopyResultsList"/> for the properties
|
||||
/// </summary>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
Statistic OriginalValues = PropertyChangedListener && NonZeroValue ? this.Clone() : null;
|
||||
|
||||
//Total
|
||||
var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, TotalField, 0, "Total");
|
||||
TotalField = 0;
|
||||
//Copied
|
||||
var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, CopiedField, 0, "Copied");
|
||||
CopiedField = 0;
|
||||
//Extras
|
||||
var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, ExtrasField, 0, "Extras");
|
||||
ExtrasField = 0;
|
||||
//Failed
|
||||
var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, FailedField, 0, "Failed");
|
||||
FailedField = 0;
|
||||
//Mismatch
|
||||
var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, MismatchField, 0, "Mismatch");
|
||||
MismatchField = 0;
|
||||
//Skipped
|
||||
var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, SkippedField, 0, "Skipped");
|
||||
SkippedField = 0;
|
||||
|
||||
//Trigger Events
|
||||
TriggerDeferredEvents(OriginalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped);
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region < Trigger Deferred Events >
|
||||
|
||||
/// <summary>
|
||||
/// Prep Event Args for SETTERS of the properties
|
||||
/// </summary>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
private Lazy<Tuple<StatChangedEventArg, StatisticPropertyChangedEventArgs>> PrepEventArgs(long OldValue, long NewValue, string PropertyName)
|
||||
{
|
||||
if (!EnablePropertyChangeEvent) return null;
|
||||
var old = this.Clone();
|
||||
return new Lazy<Tuple<StatChangedEventArg, StatisticPropertyChangedEventArgs>>(() =>
|
||||
{
|
||||
StatChangedEventArg e1 = new StatChangedEventArg(this, OldValue, NewValue, PropertyName);
|
||||
var e2 = new StatisticPropertyChangedEventArgs(this, old, PropertyName);
|
||||
return new Tuple<StatChangedEventArg, StatisticPropertyChangedEventArgs>(e1, e2);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prep event args for the ADD and RESET methods
|
||||
/// </summary>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
private StatChangedEventArg ConditionalPrepEventArgs(bool Listener, long OldValue, long NewValue, string PropertyName)=> !Listener || OldValue == NewValue ? null : new StatChangedEventArg(this, OldValue, NewValue, PropertyName);
|
||||
|
||||
/// <summary>
|
||||
/// Raises the events that were deferred while item was object was still being calculated by ADD / RESET
|
||||
/// </summary>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
private void TriggerDeferredEvents(Statistic OriginalValues, StatChangedEventArg eTotal, StatChangedEventArg eCopied, StatChangedEventArg eExtras, StatChangedEventArg eFailed, StatChangedEventArg eMismatch, StatChangedEventArg eSkipped)
|
||||
{
|
||||
//Perform Events
|
||||
int i = 0;
|
||||
if (eTotal != null)
|
||||
{
|
||||
i = 1;
|
||||
OnTotalChanged?.Invoke(this, eTotal);
|
||||
}
|
||||
if (eCopied != null)
|
||||
{
|
||||
i += 2;
|
||||
OnCopiedChanged?.Invoke(this, eCopied);
|
||||
}
|
||||
if (eExtras != null)
|
||||
{
|
||||
i += 4;
|
||||
OnExtrasChanged?.Invoke(this, eExtras);
|
||||
}
|
||||
if (eFailed != null)
|
||||
{
|
||||
i += 8;
|
||||
OnFailedChanged?.Invoke(this, eFailed);
|
||||
}
|
||||
if (eMismatch != null)
|
||||
{
|
||||
i += 16;
|
||||
OnMisMatchChanged?.Invoke(this, eMismatch);
|
||||
}
|
||||
if (eSkipped != null)
|
||||
{
|
||||
i += 32;
|
||||
OnSkippedChanged?.Invoke(this, eSkipped);
|
||||
}
|
||||
|
||||
//Trigger PropertyChangeEvent
|
||||
if (OriginalValues != null)
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
case 1: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eTotal.PropertyName)); return;
|
||||
case 2: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eCopied.PropertyName)); return;
|
||||
case 4: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eExtras.PropertyName)); return;
|
||||
case 8: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eFailed.PropertyName)); return;
|
||||
case 16: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eMismatch.PropertyName)); return;
|
||||
case 32: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, eSkipped.PropertyName)); return;
|
||||
default: PropertyChanged?.Invoke(this, new StatisticPropertyChangedEventArgs(this, OriginalValues, String.Empty)); return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region < ADD Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Add the supplied values to this Statistic object. <br/>
|
||||
/// Events are defered until all the fields have been added together.
|
||||
/// </summary>
|
||||
/// <param name="total"></param>
|
||||
/// <param name="copied"></param>
|
||||
/// <param name="extras"></param>
|
||||
/// <param name="failed"></param>
|
||||
/// <param name="mismatch"></param>
|
||||
/// <param name="skipped"></param>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
public void Add(long total = 0, long copied = 0, long extras = 0, long failed = 0, long mismatch = 0, long skipped = 0)
|
||||
{
|
||||
//Store the original object values for the event args
|
||||
Statistic originalValues = PropertyChangedListener ? this.Clone() : null;
|
||||
|
||||
//Total
|
||||
long i = TotalField;
|
||||
TotalField += total;
|
||||
var eTotal = ConditionalPrepEventArgs(Listener_TotalChanged, i, TotalField, "Total");
|
||||
|
||||
//Copied
|
||||
i = CopiedField;
|
||||
CopiedField += copied;
|
||||
var eCopied = ConditionalPrepEventArgs(Listener_CopiedChanged, i, CopiedField, "Copied");
|
||||
|
||||
//Extras
|
||||
i = ExtrasField;
|
||||
ExtrasField += extras;
|
||||
var eExtras = ConditionalPrepEventArgs(Listener_ExtrasChanged, i, ExtrasField, "Extras");
|
||||
|
||||
//Failed
|
||||
i = FailedField;
|
||||
FailedField += failed;
|
||||
var eFailed = ConditionalPrepEventArgs(Listener_FailedChanged, i, FailedField, "Failed");
|
||||
|
||||
//Mismatch
|
||||
i = MismatchField;
|
||||
MismatchField += mismatch;
|
||||
var eMismatch = ConditionalPrepEventArgs(Listener_MismatchChanged, i, MismatchField, "Mismatch");
|
||||
|
||||
//Skipped
|
||||
i = SkippedField;
|
||||
SkippedField += skipped;
|
||||
var eSkipped = ConditionalPrepEventArgs(Listener_SkippedChanged, i, SkippedField, "Skipped");
|
||||
|
||||
//Trigger Events
|
||||
if (EnablePropertyChangeEvent)
|
||||
TriggerDeferredEvents(originalValues, eTotal, eCopied, eExtras, eFailed, eMismatch, eSkipped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the results of the supplied Statistics object to this Statistics object. <br/>
|
||||
/// Events are defered until all the fields have been added together.
|
||||
/// </summary>
|
||||
/// <param name="stat">Statistics Item to add</param>
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
public void AddStatistic(IStatistic stat)
|
||||
{
|
||||
if (stat != null && stat.Type == this.Type && stat.NonZeroValue)
|
||||
Add(stat.Total, stat.Copied, stat.Extras, stat.Failed, stat.Mismatch, stat.Skipped);
|
||||
}
|
||||
|
||||
|
||||
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
|
||||
/// <param name="enablePropertyChangedEvent"><inheritdoc cref="EnablePropertyChangeEvent" path="*"/></param>
|
||||
/// <inheritdoc cref="AddStatistic(IStatistic)"/>
|
||||
internal void AddStatistic(IStatistic stats, bool enablePropertyChangedEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangedEvent;
|
||||
AddStatistic(stats);
|
||||
EnablePropertyChangeEvent = true;
|
||||
|
||||
}
|
||||
#pragma warning restore CS1573
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Add the results of the supplied Statistics objects to this Statistics object.
|
||||
/// </summary>
|
||||
/// <param name="stats">Statistics Item to add</param>
|
||||
public void AddStatistic(IEnumerable<IStatistic> stats)
|
||||
{
|
||||
foreach (Statistic stat in stats)
|
||||
{
|
||||
EnablePropertyChangeEvent = stat == stats.Last();
|
||||
AddStatistic(stat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds <see cref="StatChangedEventArg.Difference"/> to the appropriate property based on the 'PropertyChanged' value. <br/>
|
||||
/// Will only add the value if the <see cref="StatChangedEventArg.StatType"/> == <see cref="Type"/>.
|
||||
/// </summary>
|
||||
/// <param name="eventArgs">Arg provided by either <see cref="PropertyChanged"/> or a Statistic's object's On*Changed events</param>
|
||||
public void AddStatistic(PropertyChangedEventArgs eventArgs)
|
||||
{
|
||||
//Only process the args if of the proper type
|
||||
var e = eventArgs as IStatisticPropertyChangedEventArg;
|
||||
if (e == null) return;
|
||||
|
||||
if (e.StatType != this.Type || (e.Is_StatisticPropertyChangedEventArgs && e.Is_StatChangedEventArg))
|
||||
{
|
||||
// INVALID!
|
||||
}
|
||||
else if (e.Is_StatisticPropertyChangedEventArgs)
|
||||
{
|
||||
var e1 = (StatisticPropertyChangedEventArgs)eventArgs;
|
||||
AddStatistic(e1.Difference);
|
||||
}
|
||||
else if (e.Is_StatChangedEventArg)
|
||||
{
|
||||
var e2 = (StatChangedEventArg)eventArgs;
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case "": //String.Empty means all fields have changed
|
||||
AddStatistic(e2.Sender);
|
||||
break;
|
||||
case "Copied":
|
||||
this.Copied += e2.Difference;
|
||||
break;
|
||||
case "Extras":
|
||||
this.Extras += e2.Difference;
|
||||
break;
|
||||
case "Failed":
|
||||
this.Failed += e2.Difference;
|
||||
break;
|
||||
case "Mismatch":
|
||||
this.Mismatch += e2.Difference;
|
||||
break;
|
||||
case "Skipped":
|
||||
this.Skipped += e2.Difference;
|
||||
break;
|
||||
case "Total":
|
||||
this.Total += e2.Difference;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the results of the supplied statistics objects of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="stats">Collection of <see cref="Statistic"/> objects</param>
|
||||
/// <param name="statType">Create a new Statistic object of this type.</param>
|
||||
/// <returns>New Statistics Object</returns>
|
||||
public static Statistic AddStatistics(IEnumerable<IStatistic> stats, StatType statType)
|
||||
{
|
||||
Statistic ret = new Statistic(statType);
|
||||
ret.AddStatistic(stats.Where(s => s.Type == statType) );
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
#endregion ADD
|
||||
|
||||
#region < AVERAGE Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Combine the supplied <see cref="Statistic"/> objects, then get the average.
|
||||
/// </summary>
|
||||
/// <param name="stats">Array of Stats objects</param>
|
||||
public void AverageStatistic(IEnumerable<IStatistic> stats)
|
||||
{
|
||||
this.AddStatistic(stats);
|
||||
int cnt = stats.Count() + 1;
|
||||
Total /= cnt;
|
||||
Copied /= cnt;
|
||||
Extras /= cnt;
|
||||
Failed /= cnt;
|
||||
Mismatch /= cnt;
|
||||
Skipped /= cnt;
|
||||
|
||||
}
|
||||
|
||||
/// <returns>New Statistics Object</returns>
|
||||
/// <inheritdoc cref=" AverageStatistic(IEnumerable{IStatistic})"/>
|
||||
public static Statistic AverageStatistics(IEnumerable<IStatistic> stats, StatType statType)
|
||||
{
|
||||
Statistic stat = AddStatistics(stats, statType);
|
||||
int cnt = stats.Count(s => s.Type == statType);
|
||||
if (cnt > 1)
|
||||
{
|
||||
stat.Total /= cnt;
|
||||
stat.Copied /= cnt;
|
||||
stat.Extras /= cnt;
|
||||
stat.Failed /= cnt;
|
||||
stat.Mismatch /= cnt;
|
||||
stat.Skipped /= cnt;
|
||||
}
|
||||
return stat;
|
||||
}
|
||||
|
||||
#endregion AVERAGE
|
||||
|
||||
#region < Subtract Methods >
|
||||
|
||||
/// <summary>
|
||||
/// Subtract Method used by <see cref="RoboCopyResultsList"/> <br/>
|
||||
/// Events are deferred until all value changes have completed.
|
||||
/// </summary>
|
||||
/// <param name="stat">Statistics Item to subtract</param>
|
||||
#if !NET40
|
||||
[MethodImpl(methodImplOptions: MethodImplOptions.AggressiveInlining)]
|
||||
#endif
|
||||
public void Subtract(IStatistic stat)
|
||||
{
|
||||
if (stat.Type == this.Type && stat.NonZeroValue)
|
||||
Add(-stat.Total, -stat.Copied, -stat.Extras, -stat.Failed, -stat.Mismatch, -stat.Skipped);
|
||||
}
|
||||
|
||||
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
|
||||
/// <param name="enablePropertyChangedEvent"><inheritdoc cref="EnablePropertyChangeEvent" path="*"/></param>
|
||||
/// <inheritdoc cref="Subtract(IStatistic)"/>
|
||||
internal void Subtract(IStatistic stats, bool enablePropertyChangedEvent)
|
||||
{
|
||||
EnablePropertyChangeEvent = enablePropertyChangedEvent;
|
||||
Subtract(stats);
|
||||
EnablePropertyChangeEvent = true;
|
||||
}
|
||||
#pragma warning restore CS1573
|
||||
|
||||
/// <summary>
|
||||
/// Subtract the results of the supplied Statistics objects to this Statistics object.
|
||||
/// </summary>
|
||||
/// <param name="stats">Statistics Item to subtract</param>
|
||||
public void Subtract(IEnumerable<IStatistic> stats)
|
||||
{
|
||||
foreach (Statistic stat in stats)
|
||||
{
|
||||
EnablePropertyChangeEvent = stat == stats.Last();
|
||||
Subtract(stat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="MainStat">Statistics object to clone</param>
|
||||
/// <param name="stats"><inheritdoc cref="Subtract(IStatistic)"/></param>
|
||||
/// <returns>Clone of the <paramref name="MainStat"/> object with the <paramref name="stats"/> subtracted from it.</returns>
|
||||
/// <inheritdoc cref="Subtract(IStatistic)"/>
|
||||
public static Statistic Subtract(IStatistic MainStat, IStatistic stats)
|
||||
{
|
||||
var ret = MainStat.Clone();
|
||||
ret.Subtract(stats);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endregion Subtract
|
||||
|
||||
|
||||
/// <inheritdoc cref="IStatistic.Clone"/>
|
||||
public Statistic Clone() => new Statistic(this);
|
||||
|
||||
object ICloneable.Clone() => new Statistic(this);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user