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 { // --- Definitions --- public enum InterruptionReason { None = 0, ManualRequest, CancellationRequested, OperationCanceledException, } public sealed class InterruptionData { internal InterruptionData(InterruptionReason reason) { Reason = reason; RequestedAt = DateTime.Now; } public InterruptionReason Reason { get; } public DateTime RequestedAt { get; } } [Serializable] public class InProgressException : Exception { public InProgressException() { } public InProgressException(string message) : base(message) { } public InProgressException(string message, Exception inner) : base(message, inner) { } } // --- Option Selection --- // 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 --- 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 { // --- Interruption --- private class InterruptState { public readonly Object lockable = new object(); private InterruptionData interruption = null; public bool IsInterrupted { get { lock (lockable) { return interruption != null; } } } public InterruptionData Interruption { get { lock (lockable) { return interruption; } } } public void Clear() { lock (lockable) interruption = null; } public void Set(InterruptionReason reason) { lock (lockable) { if (interruption == null) interruption = new InterruptionData(reason); } } } private InterruptState interruption { get; } = new InterruptState(); public bool IsInterrupted { get { return interruption.IsInterrupted; } } public InterruptionData Interruption { get { return interruption.Interruption; } } public void ClearInterruption() { interruption.Clear(); } public void SetInterrupted() { interruption.Set(InterruptionReason.ManualRequest); } public void SetInterrupted(InterruptionReason reason) { interruption.Set(reason); } public void SetInterruptedByCancellation() { interruption.Set(InterruptionReason.CancellationRequested); } public void SetInterruptedByOperationCanceledException() { interruption.Set(InterruptionReason.OperationCanceledException); } // --- 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 Data --- private SacsTaskData taskData = null; public SacsTaskData TaskData { get { return taskData; } set { if (taskData != null) taskData.Dispose(); taskData = value; } } public bool DetachTaskData(SacsTaskData expected) { if (!Object.ReferenceEquals(taskData, expected)) return false; taskData = null; return true; } } // --- 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) { AsyncProcessData asyncData; lock (AsyncLock) { asyncData = AsyncData; if (asyncData == null) Control.TaskData = null; } // A running worker owns its task data until it exits. asyncData?.RequestCancellation(); } } // --- State --- 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; } } // --- Process Metadata --- /// 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(); } } // --- Asynchronous Execution --- private class AsyncProcessData : SacsDisposable { private readonly Object CancellationLock = new object(); public AsyncProcessData(SacsTaskData taskData, ProcessControlData control) { this.TaskData = taskData; this.Control = control; } protected override void Dispose(bool disposing) { if (disposing) { lock (CancellationLock) TaskData.Dispose(); } } public Task Task { get; set; } public SacsTaskData TaskData { get; } private ProcessControlData Control { get; } public bool RequestCancellation() { lock (CancellationLock) { if (!TaskData.CanBeCancelled) return false; Control.SetInterruptedByCancellation(); return TaskData.TriggerCancellation(); } } } private readonly Object AsyncLock = new object(); private AsyncProcessData AsyncData { get; set; } private bool InlineRunActive { get; set; } private TaskCompletionSource InlineCompletion { get; set; } public bool IsAsync { get { lock (AsyncLock) { return AsyncData != null; } } } private bool IsRunActive { get { return AsyncData != null || InlineRunActive; } } private void ClearAsyncData(AsyncProcessData asyncData) { bool clear = false; lock (AsyncLock) { // Only the task that owns the current state may clear it. if (Object.ReferenceEquals(AsyncData, asyncData)) { Control.DetachTaskData(asyncData.TaskData); AsyncData = null; clear = true; } } // Dispose outside the lifecycle lock. Disposal waits for any cancellation callback. if (clear) asyncData.Dispose(); } /// /// Run the process asynchronously. /// /// /// If process is already running. public async Task RunAsync() { AsyncProcessData asyncData; lock (AsyncLock) { if (IsRunActive) throw new InProgressException(); SacsTaskData taskData = new SacsTaskData(); Control.TaskData = taskData; // Construct before starting so the process state is visible atomically. asyncData = new AsyncProcessData(taskData, Control); Task task = new Task(() => { try { // Execute the process. RunProcessCore(false); } finally { // The worker owns its state until it has actually terminated. ClearAsyncData(asyncData); } }); asyncData.Task = task; AsyncData = asyncData; task.Start(); } await asyncData.Task; } // --- Inline Execution --- public bool RunProcess() { lock (AsyncLock) { if (IsRunActive) throw new InProgressException(); InlineRunActive = true; InlineCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } try { return RunProcessCore(true); } finally { TaskCompletionSource inlineCompletion; lock (AsyncLock) { InlineRunActive = false; inlineCompletion = InlineCompletion; InlineCompletion = null; } inlineCompletion?.TrySetResult(true); } } private bool RunProcessCore(bool clearTaskData) { // 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 (OperationCanceledException) { Control.SetInterruptedByOperationCanceledException(); } catch (Exception exc) { String msg = ExceptionUtil.GetErrorStateMessage(exc); AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} ProcessStarting: {msg}{exc.GetStackEntries(6).Concat("", "\n\tat ")}", true); } // Process running. try { // Run process. runProcess = Run(); } catch (OperationCanceledException) { Control.SetInterruptedByOperationCanceledException(); } catch (Exception exc) { String msg = ExceptionUtil.GetErrorStateMessage(exc); AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} " + msg + exc.GetStackEntries(6).Concat("", "\n\tat "), true); } finally { // Processing completed. FinishedAt = DateTime.Now; Processing = false; ProcessEnded = true; if (clearTaskData) Control.TaskData = null; } // Process completed. try { // Notify completion of process and log/ignore exceptions. InvokeProcessCompleted(); } catch (OperationCanceledException) { Control.SetInterruptedByOperationCanceledException(); } catch (Exception exc) { String msg = ExceptionUtil.GetErrorStateMessage(exc); AddErrorMessage(msg); SaLogger.Error($"{SysUtil.Fn} ProcessCompleted: {msg}{exc.GetStackEntries(6).Concat("", "\n\tat ")}", true); } return runProcess; } protected abstract bool Run(); // --- Cancellation --- /// True if no process is running or the process stopped within the timeout. public async Task StopProcessAsync(int timeout = 50) { AsyncProcessData asyncData; Task inlineCompletion; lock (AsyncLock) { asyncData = AsyncData; inlineCompletion = InlineCompletion?.Task; } if (asyncData == null && inlineCompletion == null) return true; Task processTask; if (asyncData != null) { // Cancellation may invoke callbacks synchronously, so request it outside AsyncLock. // Cancellation is cooperative; retain the process state until the worker exits. asyncData.RequestCancellation(); processTask = asyncData.Task; } else { if (!Interrupt()) return false; processTask = inlineCompletion; } if (await Task.WhenAny(processTask, Task.Delay(Math.Max(20, timeout))) != processTask) { return false; } await processTask; return true; } // --- 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 State --- public bool OK { get { if (HasErrorMessage()) return false; if (IsInterrupted) return false; return true; } } private void Reset() { Control.ClearProcessMessages(); Control.ClearInterruption(); ProcessEnded = false; StartedAt = DateTime.MinValue; FinishedAt = DateTime.MinValue; } // --- Progress Reporting --- 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.IsInterrupted || (Control.TaskData?.IsCancellationRequested ?? false); } } public InterruptionData Interruption { get { return Control.Interruption; } } 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.SetInterrupted(); return true; } protected virtual bool IsInterruptionConfirmed() { return true; } // --- The End --- } }