Files
Common-VS2019/SacsStandardBase/Base/Procs/SaProcess_Base.cs
T

655 lines
19 KiB
C#

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<OptionData> Options { get; } = new List<OptionData>();
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()
{
}
/// <summary>Identifies the type of process.</summary>
public ProcessType ProcessType { get; set; } = ProcessType.Unknown;
/// <summary>Is the process to be run in 'Test' mode.</summary>
public bool TestProcess { get; set; } = SacsSystem.DebugMode;
/// <summary>Process imports data.</summary>
public bool Import { get { return ProcessType == ProcessType.ImportData || ProcessType == ProcessType.ImportExportData; } }
/// <summary>Process exports data.</summary>
public bool Export { get { return ProcessType == ProcessType.ExportData || ProcessType == ProcessType.ImportExportData; } }
/// <summary>Can the process be interrupted?</summary>
public bool AllowInterruption { get; set; } = true;
/// <summary>Confirm when interruption is requested.</summary>
public bool ConfirmInterruption { get; set; } = true;
/// <summary>Automated processes must not display desktop coponents - popup messages etc.</summary>
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<ProcessMessage> messages { get; } = new List<ProcessMessage>();
}
private ProcessMessagesData processMessages { get; } = new ProcessMessagesData();
/// <summary>Convert the message levele to one of the 3 values used with Process Messages: Info, Warning, Error.</summary>
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;
}
/// <summary>Add a message of the specified level (Info, Warning, Error).</summary>
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;
}
/// <summary>Are there any message of the specified level (Info, Warning, Error).</summary>
public bool HasProcessMessages(SaMessageLevel level)
{
lock (processMessages.lockable)
return processMessages.messages.Exists(item => item.Level == GetLevel(level));
}
/// <summary>Get the first message of the specified level (Info, Warning, Error).</summary>
public String GetProcessMessage(SaMessageLevel level)
{
lock (processMessages.lockable)
return processMessages.messages.Find(item => item.Level == GetLevel(level))?.Message ?? "";
}
/// <summary>Return all process messages.</summary>
public List<ProcessMessage> GetProcessMessages()
{
List<ProcessMessage> messages = new List<ProcessMessage>();
lock (processMessages.lockable)
messages.AddRange(processMessages.messages);
return messages;
}
/// <summary>Return all process messages of the specified Level (Info, Warning, Error).</summary>
public List<ProcessMessage> GetProcessMessages(SaMessageLevel level)
{
List<ProcessMessage> messages = new List<ProcessMessage>();
lock (processMessages.lockable)
messages.AddRange(processMessages.messages.FindAll(item => item.Level == GetLevel(level)));
return messages;
}
/// <summary>Clear process messages.</summary>
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 ---
/// <summary>Process caption.</summary>
public abstract String ProcessDescription { get; }
/// <summary>Process comments - to be displayed with process selection.</summary>
public virtual List<String> ProcessComments { get { return new List<String>(); } }
/// <summary>Processing statistics.</summary>
public virtual List<StringPair> ProcessingStatistics { get { return new List<StringPair>(); } }
// --- 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<Boolean> 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();
}
/// <summary>
/// Run the process asynchronously.
/// </summary>
/// <returns></returns>
/// <exception cref="InProgressException">If process is already running.</exception>
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<Boolean>(TaskCreationOptions.RunContinuationsAsynchronously);
}
try
{
return RunProcessCore(true);
}
finally
{
TaskCompletionSource<Boolean> 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 ---
/// <returns>True if no process is running or the process stopped within the timeout.</returns>
public async Task<bool> 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<ProcessStartingEventArgs> OnProcessStarting;
protected virtual void InvokeProcessStarting()
{
if (OnProcessStarting != null)
{
ProcessStartingEventArgs args = new ProcessStartingEventArgs();
OnProcessStarting(this, args);
}
}
// Process Info Updated.
public class ProgressInfoUpdatedEventArgs : EventArgs
{
public List<String> ProgressInfo { get; } = new List<string>();
}
public event EventHandler<ProgressInfoUpdatedEventArgs> OnProgressInfoUpdated;
protected virtual void InvokeProgressInfoUpdated(List<String> progressInfo)
{
if (OnProgressInfoUpdated != null)
{
ProgressInfoUpdatedEventArgs args = new ProgressInfoUpdatedEventArgs();
args.ProgressInfo.AddRange(progressInfo);
OnProgressInfoUpdated(this, args);
}
}
// Processing Completed.
public class ProcessCompletedEventArgs : EventArgs { }
public event EventHandler<ProcessCompletedEventArgs> 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<String> progressInfo)
{
InvokeProgressInfoUpdated(progressInfo);
}
// --- Process Messages ---
/// <summary>Add a message of the specified level (Info, Warning, Error).</summary>
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);
}
/// <summary>Add the error message and always return false.</summary>
protected bool AddErrorMessage(String message)
{
AddMessage(message, SaMessageLevel.Error);
return false;
}
/// <summary>Are there any error messages.</summary>
public bool HasErrorMessage()
{
return Control.HasProcessMessages(SaMessageLevel.Error);
}
/// <summary>Get the first error message.</summary>
public String GetErrorMessage()
{
return Control.GetProcessMessage(SaMessageLevel.Error);
}
/// <summary>Return all process messages.</summary>
public List<ProcessMessage> GetProcessMessages()
{
return Control.GetProcessMessages();
}
/// <summary>Return all process messages of the specified Level (Info, Warning, Error).</summary>
public List<ProcessMessage> GetProcessMessages(SaMessageLevel level)
{
return Control.GetProcessMessages(level);
}
/// <summary>Clear process messages.</summary>
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 ---
}
}