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:
maier_S
2022-03-23 14:17:56 +01:00
parent a0095a0516
commit 907ad039c4
57 changed files with 11301 additions and 0 deletions

View 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
}
}

View 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;
}
}
}

View 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,
}
}

View 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;
}
}
}

View 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
}
}

View 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();
}
}

View 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
}
}

View 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
}
}

View 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
}
}

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