Harden process interruption lifecycle

This commit is contained in:
Andy Howard
2026-09-17 17:07:43 +01:00
parent a0a5ef970c
commit 0406adfc99
4 changed files with 327 additions and 72 deletions
+151 -62
View File
@@ -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 ---
}
//=======================================================================
}