using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Core; using Sarand.Net.Standard.Base.Tasks; using Sarand.Net.Standard.Base.Utils; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Sarand.Net.Standard.Base.Procs { //======================================================================= public abstract class SaProcess_Base : SacsDisposable { //------------------------------------------------------------------- // Exceptions. [Serializable] public class InProgressException : Exception { public InProgressException() { } public InProgressException(string message) : base(message) { } public InProgressException(string message, Exception inner) : base(message, inner) { } } //------------------------------------------------------------------- // Process options and groups - for configuration of process to run. // See Sarand.Win.Forms.Utility.SaForm_SelectOptions. public class OptionData { public OptionData(TypeCodeDesc option) { Option = option; } public TypeCodeDesc Option { get; set; } public String Notes { get; set; } = ""; public bool Selected { get; set; } = false; } public class GroupData { public GroupData(TypeCodeDesc optionGroup) { OptionGroup = optionGroup; } public TypeCodeDesc OptionGroup { get; set; } public List Options { get; } = new List(); public bool ExclusiveOptions { get; set; } = false; public bool Selected { get; set; } = false; } //------------------------------------------------------------------- // Configuration. public enum ProcessType { Unknown = 0, ImportData, ExportData, ImportExportData, Other, } public class Config { protected Config() { } /// Identifies the type of process. public ProcessType ProcessType { get; set; } = ProcessType.Unknown; /// Is the process to be run in 'Test' mode. public bool TestProcess { get; set; } = SacsSystem.DebugMode; /// Process imports data. public bool Import { get { return ProcessType == ProcessType.ImportData || ProcessType == ProcessType.ImportExportData; } } /// Process exports data. public bool Export { get { return ProcessType == ProcessType.ExportData || ProcessType == ProcessType.ImportExportData; } } /// Can the process be interrupted? public bool AllowInterruption { get; set; } = true; /// Confirm when interruption is requested. public bool ConfirmInterruption { get; set; } = true; /// Automated processes must not display desktop coponents - popup messages etc. public bool Automated { get; set; } = false; } //------------------------------------------------------------------- // Process control definitions. public class ProcessMessage { public ProcessMessage(String message, SaMessageLevel level = SaMessageLevel.Info) { this.Message = message; this.Level = level; } public String Message { get; } public SaMessageLevel Level { get; } public int Instances { get; internal set; } } private class ProcessControlData { // Interrupt. private class InterruptData { public readonly Object lockable = new object(); private bool interrupted = false; public bool Interrupted { get { lock (lockable) { return interrupted; } } set { lock (lockable) { interrupted = value; } } } } private InterruptData interrupt { get; } = new InterruptData(); public bool Interrupted { get { return interrupt.Interrupted; } set { interrupt.Interrupted = value; } } // Process messages. private class ProcessMessagesData { public readonly Object lockable = new object(); public List messages { get; } = new List(); } private ProcessMessagesData processMessages { get; } = new ProcessMessagesData(); /// Convert the message levele to one of the 3 values used with Process Messages: Info, Warning, Error. public static SaMessageLevel GetLevel(SaMessageLevel level) { if (level == SaMessageLevel.Error || level == SaMessageLevel.Fatal || level == SaMessageLevel.Exception) level = SaMessageLevel.Error; else if (level == SaMessageLevel.Warning) level = SaMessageLevel.Warning; else level = SaMessageLevel.Info; return level; } /// Add a message of the specified level (Info, Warning, Error). public bool AddProcessMessage(String message, SaMessageLevel level) { message = StringUtil.TrimWhitespace(message); if (StringUtil.IsBlank(message)) return false; level = GetLevel(level); lock (processMessages.lockable) { ProcessMessage procMsg = processMessages.messages.Find(item => item.Message.EqualsCI(message) && item.Level == level); // Duplicate messages increment the 'Instances' value. if (procMsg != null) procMsg.Instances++; // Otherwise add a new message to the list. else processMessages.messages.Add(new ProcessMessage(message, level)); } return true; } /// Are there any message of the specified level (Info, Warning, Error). public bool HasProcessMessages(SaMessageLevel level) { lock (processMessages.lockable) return processMessages.messages.Exists(item => item.Level == GetLevel(level)); } /// Get the first message of the specified level (Info, Warning, Error). public String GetProcessMessage(SaMessageLevel level) { lock (processMessages.lockable) return processMessages.messages.Find(item => item.Level == GetLevel(level))?.Message ?? ""; } /// Return all process messages. public List GetProcessMessages() { List messages = new List(); lock (processMessages.lockable) messages.AddRange(processMessages.messages); return messages; } /// Return all process messages of the specified Level (Info, Warning, Error). public List GetProcessMessages(SaMessageLevel level) { List messages = new List(); lock (processMessages.lockable) messages.AddRange(processMessages.messages.FindAll(item => item.Level == GetLevel(level))); return messages; } /// Clear process messages. public void ClearProcessMessages() { lock (processMessages.lockable) processMessages.messages.Clear(); } // Task control data. private SacsTaskData taskData = null; public SacsTaskData TaskData { get { return taskData; } set { if (taskData != null) taskData.Dispose(); taskData = value; } } } //------------------------------------------------------------------- // Data members. private Config config; private ProcessControlData Control { get; } = new ProcessControlData(); //------------------------------------------------------------------- // Construction. protected SaProcess_Base(Config config) { this.config = config; } protected override void Dispose(bool disposing) { if (disposing) { // Clear process data to dispose. Control.TaskData = null; } } //------------------------------------------------------------------- // Properties. public bool DebugMode { get { return SacsSystem.DebugMode; } } public bool Processing { get; protected set; } = false; public bool ProcessEnded { get; protected set; } = false; public DateTime StartedAt { get; private set; } = DateTime.MinValue; public DateTime FinishedAt { get; private set; } = DateTime.MinValue; public int ElapsedSecs { get { if (DateUtil.IsValid(StartedAt)) return (int)((DateUtil.IsValid(FinishedAt) ? FinishedAt : DateTime.Now) - StartedAt).TotalSeconds; return 0; } } //------------------------------------------------------------------- // Properties. /// Process caption. public abstract String ProcessDescription { get; } /// Process comments - to be displayed with process selection. public virtual List ProcessComments { get { return new List(); } } /// Processing statistics. public virtual List ProcessingStatistics { get { return new List(); } } //------------------------------------------------------------------- // Run process asynchronously. private class AsyncProcessData : SacsDisposable { public AsyncProcessData(Task task) { this.Task = task; } protected override void Dispose(bool disposing) { if (disposing) { } } public Task Task { get; } } private AsyncProcessData AsyncData { get; set; } public bool IsAsync { get { return AsyncData != null; } } private void ClearAsyncData() { if (AsyncData != null) { lock (AsyncData) { AsyncData.Dispose(); AsyncData = null; } } } /// /// Run the process asynchronously. /// /// /// If process is already running. public async void RunAsync() { if (AsyncData != null) throw new InProgressException(); Control.TaskData = new SacsTaskData(); // Run on separate thread. Task task = new Task(() => { try { // Execute the process. RunProcess(); } catch (OperationCanceledException exc) { SaLogger.Warn(exc.GetType().Name + " during asynchronous processing on " + GetType().Name); } finally { // Clear processing data. ClearAsyncData(); } }, Control.TaskData.CancellationToken); AsyncData = new AsyncProcessData(task); task.Start(); await task; } //------------------------------------------------------------------- // Run process inline. public bool RunProcess() { // Prepare process. bool runProcess = false; Reset(); if (Control.TaskData == null) Control.TaskData = new SacsTaskData(); Processing = true; StartedAt = DateTime.Now; // Process starting. try { // Notify start of process and log/ignore exceptions. InvokeProcessStarting(); } catch (Exception exc) { String msg = exc.GetType().Name + ": " + exc.Message; AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} ProcessStarting: {msg}{exc.GetStackEntries(6).Concat("", "\n\tat ")}", true); } // Process running. try { // Run process. runProcess = Run(); } catch (Exception exc) { String msg = exc.GetType().Name + ": " + exc.Message; AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} " + msg + exc.GetStackEntries(6).Concat("", "\n\tat "), true); } finally { // Processing completed. FinishedAt = DateTime.Now; Processing = false; ProcessEnded = true; Control.TaskData = null; } // Process completed. try { // Notify completion of process and log/ignore exceptions. InvokeProcessCompleted(); } catch (Exception exc) { String msg = exc.GetType().Name + ": " + exc.Message; AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} ProcessCompleted: {msg}{exc.GetStackEntries(6).Concat("", "\n\tat ")}", true); } return runProcess; } protected abstract bool Run(); //------------------------------------------------------------------- public async Task StopProcessAsync(int timeout = 50) { if (!Processing) return; if (AsyncData != null) { try { // Cancel the task. Control.TaskData.TriggerCancellation(); // Wait for task termination or timeout. if (await Task.WhenAny(AsyncData.Task, Task.Delay(Math.Max(20, Math.Min(1000, timeout)))) != AsyncData.Task) { // Timeout completed before task - force termination. Control.TaskData.ThrowIfCancellationRequested(); } } finally { Control.TaskData = null; ClearAsyncData(); } } } //------------------------------------------------------------------- // Processing events. // Processing Starting. public class ProcessStartingEventArgs : EventArgs { } public event EventHandler OnProcessStarting; protected virtual void InvokeProcessStarting() { if (OnProcessStarting != null) { ProcessStartingEventArgs args = new ProcessStartingEventArgs(); OnProcessStarting(this, args); } } // Process Info Updated. public class ProgressInfoUpdatedEventArgs : EventArgs { public List ProgressInfo { get; } = new List(); } public event EventHandler OnProgressInfoUpdated; protected virtual void InvokeProgressInfoUpdated(List progressInfo) { if (OnProgressInfoUpdated != null) { ProgressInfoUpdatedEventArgs args = new ProgressInfoUpdatedEventArgs(); args.ProgressInfo.AddRange(progressInfo); OnProgressInfoUpdated(this, args); } } // Processing Completed. public class ProcessCompletedEventArgs : EventArgs { } public event EventHandler OnProcessCompleted; protected virtual void InvokeProcessCompleted() { if (OnProcessCompleted != null) { ProcessCompletedEventArgs args = new ProcessCompletedEventArgs(); OnProcessCompleted(this, args); } } //------------------------------------------------------------------- // Process control. public bool OK { get { if (HasErrorMessage()) return false; if (Control.Interrupted) return false; return true; } } private void Reset() { Control.ClearProcessMessages(); Control.Interrupted = false; ProcessEnded = false; StartedAt = DateTime.MinValue; FinishedAt = DateTime.MinValue; } //------------------------------------------------------------------- // Progress information. protected void SetProgressInfo(String progressInfo) { SetProgressInfo(StringListUtil.CreateList(progressInfo)); } protected void SetProgressInfo(List progressInfo) { InvokeProgressInfoUpdated(progressInfo); } //------------------------------------------------------------------- // Process messages. /// Add a message of the specified level (Info, Warning, Error). protected void AddMessage(String message, SaMessageLevel level) { if (Control.AddProcessMessage(message, level)) SaLogger.Log(GetType().Name + " " + message, ProcessControlData.GetLevel(level)); } protected void AddInfoMessage(String message) { AddMessage(message, SaMessageLevel.Info); } protected void AddWarningMessage(String message) { AddMessage(message, SaMessageLevel.Warning); } /// Add the error message and always return false. protected bool AddErrorMessage(String message) { AddMessage(message, SaMessageLevel.Error); return false; } /// Are there any error messages. public bool HasErrorMessage() { return Control.HasProcessMessages(SaMessageLevel.Error); } /// Get the first error message. public String GetErrorMessage() { return Control.GetProcessMessage(SaMessageLevel.Error); } /// Return all process messages. public List GetProcessMessages() { return Control.GetProcessMessages(); } /// Return all process messages of the specified Level (Info, Warning, Error). public List GetProcessMessages(SaMessageLevel level) { return Control.GetProcessMessages(level); } /// Clear process messages. public void ClearProcessMessages() { Control.ClearProcessMessages(); } //------------------------------------------------------------------- // Interruption. public bool IsInterrupted { get { return Control.Interrupted; } } public bool Interrupt() { if (!config.AllowInterruption) { SaLogger.Warn("SacsProcess.Interrupt() Interruptions not allowed in " + this.GetType().Name); return false; } if (config.ConfirmInterruption && !IsInterruptionConfirmed()) return false; Control.Interrupted = true; return true; } protected virtual bool IsInterruptionConfirmed() { return true; } //------------------------------------------------------------------- } //======================================================================= }