524 lines
16 KiB
C#
524 lines
16 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
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// 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 = 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 (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;
|
|
Control.TaskData = null;
|
|
}
|
|
|
|
// Process completed.
|
|
try
|
|
{
|
|
// Notify completion of process and log/ignore exceptions.
|
|
InvokeProcessCompleted();
|
|
}
|
|
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();
|
|
|
|
//-------------------------------------------------------------------
|
|
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;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|