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