Files
Common-VS2019/SacsStandardBase/Base/Procs/SaProcess_Base.cs
T
2024-08-20 17:51:51 +01:00

528 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
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 static Sarand.Net.Standard.Base.Procs.SaProcess_Base;
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<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 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<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 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.
/// <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>(); } }
//-------------------------------------------------------------------
// 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;
}
}
}
/// <summary>
/// Run the process asynchronously.
/// </summary>
/// <returns></returns>
/// <exception cref="InProgressException">If process is already running.</exception>
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<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 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<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.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;
}
//-------------------------------------------------------------------
}
//=======================================================================
}