Harden process interruption lifecycle
This commit is contained in:
@@ -9,11 +9,30 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Sarand.Net.Standard.Base.Procs
|
||||
{
|
||||
//=======================================================================
|
||||
public abstract class SaProcess_Base : SacsDisposable
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// Exceptions.
|
||||
// --- 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
|
||||
{
|
||||
@@ -22,7 +41,8 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
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
|
||||
@@ -47,8 +67,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
public bool Selected { get; set; } = false;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Configuration.
|
||||
// --- Configuration ---
|
||||
public enum ProcessType
|
||||
{
|
||||
Unknown = 0,
|
||||
@@ -78,8 +97,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
public bool Automated { get; set; } = false;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Process control definitions.
|
||||
// --- Process Control ---
|
||||
|
||||
public class ProcessMessage
|
||||
{
|
||||
@@ -95,17 +113,39 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
|
||||
private class ProcessControlData
|
||||
{
|
||||
// Interrupt.
|
||||
private class InterruptData
|
||||
// --- Interruption ---
|
||||
|
||||
private class InterruptState
|
||||
{
|
||||
public readonly Object lockable = new object();
|
||||
private bool interrupted = false;
|
||||
public bool Interrupted { get { lock (lockable) { return interrupted; } } set { lock (lockable) { interrupted = value; } } }
|
||||
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 InterruptData interrupt { get; } = new InterruptData();
|
||||
public bool Interrupted { get { return interrupt.Interrupted; } set { interrupt.Interrupted = value; } }
|
||||
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 ---
|
||||
|
||||
// Process messages.
|
||||
private class ProcessMessagesData
|
||||
{
|
||||
public readonly Object lockable = new object();
|
||||
@@ -175,7 +215,8 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
processMessages.messages.Clear();
|
||||
}
|
||||
|
||||
// Task control data.
|
||||
// --- 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)
|
||||
@@ -186,13 +227,11 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Data members.
|
||||
// --- Data Members ---
|
||||
private Config config;
|
||||
private ProcessControlData Control { get; } = new ProcessControlData();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Construction.
|
||||
// --- Construction ---
|
||||
protected SaProcess_Base(Config config)
|
||||
{
|
||||
this.config = config;
|
||||
@@ -213,8 +252,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Properties.
|
||||
// --- State ---
|
||||
public bool DebugMode { get { return SacsSystem.DebugMode; } }
|
||||
public bool Processing { get; protected set; } = false;
|
||||
public bool ProcessEnded { get; protected set; } = false;
|
||||
@@ -230,8 +268,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Properties.
|
||||
// --- Process Metadata ---
|
||||
/// <summary>Process caption.</summary>
|
||||
public abstract String ProcessDescription { get; }
|
||||
/// <summary>Process comments - to be displayed with process selection.</summary>
|
||||
@@ -239,15 +276,15 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
/// <summary>Processing statistics.</summary>
|
||||
public virtual List<StringPair> ProcessingStatistics { get { return new List<StringPair>(); } }
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Run process asynchronously.
|
||||
// --- Asynchronous Execution ---
|
||||
private class AsyncProcessData : SacsDisposable
|
||||
{
|
||||
private readonly Object CancellationLock = new object();
|
||||
|
||||
public AsyncProcessData(SacsTaskData taskData)
|
||||
public AsyncProcessData(SacsTaskData taskData, ProcessControlData control)
|
||||
{
|
||||
this.TaskData = taskData;
|
||||
this.Control = control;
|
||||
}
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
@@ -259,15 +296,27 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
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;
|
||||
@@ -295,30 +344,26 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
AsyncProcessData asyncData;
|
||||
lock (AsyncLock)
|
||||
{
|
||||
if (AsyncData != null)
|
||||
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);
|
||||
asyncData = new AsyncProcessData(taskData, Control);
|
||||
Task task = new Task(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Execute the process.
|
||||
RunProcess(false);
|
||||
}
|
||||
catch (OperationCanceledException exc)
|
||||
{
|
||||
SaLogger.Warn(exc.GetType().Name + " during asynchronous processing on " + GetType().Name);
|
||||
RunProcessCore(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The worker owns its state until it has actually terminated.
|
||||
ClearAsyncData(asyncData);
|
||||
}
|
||||
}, taskData.CancellationToken);
|
||||
});
|
||||
asyncData.Task = task;
|
||||
AsyncData = asyncData;
|
||||
task.Start();
|
||||
@@ -326,13 +371,34 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
await asyncData.Task;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Run process inline.
|
||||
// --- Inline Execution ---
|
||||
public bool RunProcess()
|
||||
{
|
||||
return RunProcess(true);
|
||||
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 RunProcess(bool clearTaskData)
|
||||
private bool RunProcessCore(bool clearTaskData)
|
||||
{
|
||||
// Prepare process.
|
||||
bool runProcess = false;
|
||||
@@ -348,6 +414,10 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Notify start of process and log/ignore exceptions.
|
||||
InvokeProcessStarting();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Control.SetInterruptedByOperationCanceledException();
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
String msg = ExceptionUtil.GetErrorStateMessage(exc);
|
||||
@@ -361,6 +431,10 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Run process.
|
||||
runProcess = Run();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Control.SetInterruptedByOperationCanceledException();
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
String msg = ExceptionUtil.GetErrorStateMessage(exc);
|
||||
@@ -383,6 +457,10 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Notify completion of process and log/ignore exceptions.
|
||||
InvokeProcessCompleted();
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Control.SetInterruptedByOperationCanceledException();
|
||||
}
|
||||
catch (Exception exc)
|
||||
{
|
||||
String msg = ExceptionUtil.GetErrorStateMessage(exc);
|
||||
@@ -393,30 +471,45 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
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;
|
||||
if (asyncData == null)
|
||||
inlineCompletion = InlineCompletion?.Task;
|
||||
}
|
||||
if (asyncData == null && inlineCompletion == null)
|
||||
return true;
|
||||
|
||||
// Cancellation may invoke callbacks synchronously, so request it outside AsyncLock.
|
||||
// Cancellation is cooperative; retain the process state until the worker exits.
|
||||
asyncData.RequestCancellation();
|
||||
if (await Task.WhenAny(asyncData.Task, Task.Delay(Math.Max(20, timeout))) != asyncData.Task)
|
||||
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 asyncData.Task;
|
||||
await processTask;
|
||||
return true;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Processing events.
|
||||
// --- Processing Events ---
|
||||
|
||||
// Processing Starting.
|
||||
public class ProcessStartingEventArgs : EventArgs { }
|
||||
@@ -458,28 +551,26 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Process control.
|
||||
// --- Process State ---
|
||||
public bool OK
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasErrorMessage()) return false;
|
||||
if (Control.Interrupted) return false;
|
||||
if (IsInterrupted) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
private void Reset()
|
||||
{
|
||||
Control.ClearProcessMessages();
|
||||
Control.Interrupted = false;
|
||||
Control.ClearInterruption();
|
||||
ProcessEnded = false;
|
||||
StartedAt = DateTime.MinValue;
|
||||
FinishedAt = DateTime.MinValue;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Progress information.
|
||||
// --- Progress Reporting ---
|
||||
protected void SetProgressInfo(String progressInfo)
|
||||
{
|
||||
SetProgressInfo(StringListUtil.CreateList(progressInfo));
|
||||
@@ -489,8 +580,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
InvokeProgressInfoUpdated(progressInfo);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Process messages.
|
||||
// --- Process Messages ---
|
||||
/// <summary>Add a message of the specified level (Info, Warning, Error).</summary>
|
||||
protected void AddMessage(String message, SaMessageLevel level)
|
||||
{
|
||||
@@ -537,12 +627,12 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
Control.ClearProcessMessages();
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Interruption.
|
||||
// --- Interruption ---
|
||||
public bool IsInterrupted
|
||||
{
|
||||
get { return Control.Interrupted || (Control.TaskData?.IsCancellationRequested ?? false); }
|
||||
get { return Control.IsInterrupted || (Control.TaskData?.IsCancellationRequested ?? false); }
|
||||
}
|
||||
public InterruptionData Interruption { get { return Control.Interruption; } }
|
||||
public bool Interrupt()
|
||||
{
|
||||
if (!config.AllowInterruption)
|
||||
@@ -551,7 +641,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
return false;
|
||||
}
|
||||
if (config.ConfirmInterruption && !IsInterruptionConfirmed()) return false;
|
||||
Control.Interrupted = true;
|
||||
Control.SetInterrupted();
|
||||
return true;
|
||||
}
|
||||
protected virtual bool IsInterruptionConfirmed()
|
||||
@@ -559,7 +649,6 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
return true;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// --- The End ---
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user