Harden process interruption lifecycle
This commit is contained in:
@@ -2,6 +2,7 @@ using NUnit.Framework;
|
||||
using Sarand.Net.Standard.Base.Procs;
|
||||
using Sarand.Net.Standard.Base.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -24,9 +25,11 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
|
||||
private class TestProcess : SaProcess_Base
|
||||
{
|
||||
public TestProcess(bool stopOnCancellation) : base(new TestProcessConfig())
|
||||
public TestProcess(bool stopOnCancellation, bool throwOnCancellation = false, bool throwImmediately = false) : base(new TestProcessConfig())
|
||||
{
|
||||
StopOnCancellation = stopOnCancellation;
|
||||
ThrowOnCancellation = throwOnCancellation;
|
||||
ThrowImmediately = throwImmediately;
|
||||
}
|
||||
|
||||
public override String ProcessDescription { get { return "Test process"; } }
|
||||
@@ -34,15 +37,21 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
public ManualResetEventSlim Release { get; } = new ManualResetEventSlim(false);
|
||||
public bool CancellationObserved { get; private set; }
|
||||
private bool StopOnCancellation { get; }
|
||||
private bool ThrowOnCancellation { get; }
|
||||
private bool ThrowImmediately { get; }
|
||||
|
||||
protected override bool Run()
|
||||
{
|
||||
Started.Set();
|
||||
if (ThrowImmediately)
|
||||
throw new OperationCanceledException();
|
||||
while (!Release.Wait(5))
|
||||
{
|
||||
if (StopOnCancellation && IsInterrupted)
|
||||
{
|
||||
CancellationObserved = true;
|
||||
if (ThrowOnCancellation)
|
||||
throw new OperationCanceledException();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +59,47 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
private class PausedTaskScheduler : TaskScheduler
|
||||
{
|
||||
private readonly Queue<Task> tasks = new Queue<Task>();
|
||||
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
{
|
||||
lock (tasks)
|
||||
return new List<Task>(tasks);
|
||||
}
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
lock (tasks)
|
||||
tasks.Enqueue(task);
|
||||
}
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
public void RunNext()
|
||||
{
|
||||
Task task;
|
||||
lock (tasks)
|
||||
{
|
||||
if (tasks.Count == 0)
|
||||
return;
|
||||
task = tasks.Dequeue();
|
||||
}
|
||||
TryExecuteTask(task);
|
||||
}
|
||||
public void RunAll()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
lock (tasks)
|
||||
if (tasks.Count == 0)
|
||||
return;
|
||||
RunNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[Test]
|
||||
@@ -85,6 +135,72 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunProcess_RejectsWhileAsyncRunIsActive()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.That(process.Started.Wait(1000), Is.True, "Process did not start.");
|
||||
|
||||
Assert.That(() => process.RunProcess(), Throws.TypeOf<SaProcess_Base.InProgressException>());
|
||||
|
||||
process.Release.Set();
|
||||
await runTask;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_RejectsWhileInlineRunIsActive()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Task<Boolean> runTask = Task.Run(() => process.RunProcess());
|
||||
Assert.That(process.Started.Wait(1000), Is.True, "Process did not start.");
|
||||
|
||||
Assert.ThrowsAsync<SaProcess_Base.InProgressException>(() => process.RunAsync());
|
||||
|
||||
process.Release.Set();
|
||||
Assert.That(await runTask, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_CancellationBeforeWorkerStarts_ClearsAsyncState()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(true))
|
||||
{
|
||||
PausedTaskScheduler scheduler = new PausedTaskScheduler();
|
||||
Task<Task> setupTask = Task.Factory.StartNew(() => process.RunAsync(), CancellationToken.None, TaskCreationOptions.None, scheduler);
|
||||
scheduler.RunNext();
|
||||
Task runTask = setupTask.GetAwaiter().GetResult();
|
||||
Assert.That(process.IsAsync, Is.True);
|
||||
|
||||
Task<bool> stopTask = process.StopProcessAsync(1000);
|
||||
scheduler.RunAll();
|
||||
|
||||
Assert.That(await stopTask, Is.True);
|
||||
await runTask;
|
||||
Assert.That(process.CancellationObserved, Is.True);
|
||||
Assert.That(process.IsAsync, Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StopProcessAsync_InterruptsInlineProcess()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(true))
|
||||
{
|
||||
Task<Boolean> runTask = Task.Run(() => process.RunProcess());
|
||||
Assert.That(process.Started.Wait(1000), Is.True, "Process did not start.");
|
||||
|
||||
Assert.That(await process.StopProcessAsync(1000), Is.True);
|
||||
Assert.That(await runTask, Is.False);
|
||||
Assert.That(process.CancellationObserved, Is.True);
|
||||
Assert.That(process.Interruption.Reason, Is.EqualTo(SaProcess_Base.InterruptionReason.ManualRequest));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StopProcessAsync_TimeoutRetainsAsyncState()
|
||||
{
|
||||
@@ -116,10 +232,64 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
|
||||
Assert.That(process.CancellationObserved, Is.True);
|
||||
Assert.That(process.IsAsync, Is.False);
|
||||
Assert.That(process.IsInterrupted, Is.True, "Cancellation must remain visible after asynchronous task data is released.");
|
||||
Assert.That(process.Interruption, Is.Not.Null);
|
||||
Assert.That(process.Interruption.Reason, Is.EqualTo(SaProcess_Base.InterruptionReason.CancellationRequested));
|
||||
Assert.That(process.OK, Is.False);
|
||||
Assert.That(await process.StopProcessAsync(20), Is.True, "Stopping an idle process should succeed.");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task RunAsync_RecordsOperationCanceledExceptionAsCancellation()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(true, true))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.That(process.Started.Wait(1000), Is.True, "Process did not start.");
|
||||
|
||||
Assert.That(await process.StopProcessAsync(1000), Is.True);
|
||||
await runTask;
|
||||
|
||||
Assert.That(process.CancellationObserved, Is.True);
|
||||
Assert.That(process.IsInterrupted, Is.True);
|
||||
Assert.That(process.Interruption.Reason, Is.EqualTo(SaProcess_Base.InterruptionReason.CancellationRequested));
|
||||
Assert.That(process.HasErrorMessage(), Is.False);
|
||||
Assert.That(process.OK, Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RunProcess_RecordsOperationCanceledExceptionReason()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false, false, true))
|
||||
{
|
||||
Assert.That(process.RunProcess(), Is.False);
|
||||
|
||||
Assert.That(process.IsInterrupted, Is.True);
|
||||
Assert.That(process.Interruption, Is.Not.Null);
|
||||
Assert.That(process.Interruption.Reason, Is.EqualTo(SaProcess_Base.InterruptionReason.OperationCanceledException));
|
||||
Assert.That(process.HasErrorMessage(), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Interrupt_RecordsManualReasonAndNextRunClearsIt()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Assert.That(process.Interrupt(), Is.True);
|
||||
Assert.That(process.IsInterrupted, Is.True);
|
||||
Assert.That(process.Interruption, Is.Not.Null);
|
||||
Assert.That(process.Interruption.Reason, Is.EqualTo(SaProcess_Base.InterruptionReason.ManualRequest));
|
||||
|
||||
process.Release.Set();
|
||||
Assert.That(process.RunProcess(), Is.True);
|
||||
Assert.That(process.IsInterrupted, Is.False);
|
||||
Assert.That(process.Interruption, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task StopProcessAsync_CancellationCallbackCanReadAsyncState()
|
||||
{
|
||||
|
||||
@@ -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 ---
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
|
||||
@@ -12,18 +12,18 @@ Last updated: 2026-09-16
|
||||
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `SacsStandardBase/Base/Utils/AssemblyUtil.cs` | PE/COFF timestamp utility | Hardened and covered | 2026-09-16 | Retain the focused tests; use the non-throwing `TryGetBuildDateTime` API where an unavailable timestamp must be distinguished from `DateTime.MinValue`. | Exact primitive reads replace unchecked partial reads and marshaling. Five tests cover a known timestamp, invalid signature, truncated input, a loaded assembly, and null input. COFF timestamps are not presented as reliable physical build times for deterministic builds. Existing framework APIs already cover assembly identity, version, and metadata, so no duplicate convenience wrappers were added. |
|
||||
| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-16 | Continue the staged package review with the remaining framework-aligned and separately tested major updates. | All supported projects target .NET 10 only. The completed package and utility updates pass the complete validation. The solution builds with zero errors and all 167 tests pass; BusinessMgr builds with 11 Control and 13 Portal tests passing. Xamarin remains isolated and unchanged. |
|
||||
| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-16 | Continue the staged package review with the remaining framework-aligned and separately tested major updates. | All supported projects target .NET 10 only. The completed package and utility updates pass the complete validation. The solution builds with zero errors and all 185 tests pass; BusinessMgr builds with 15 Control and 13 Portal tests passing. Xamarin remains isolated and unchanged. |
|
||||
| `SacsStandardBase/Base/Utils/ImageUtils.cs` | ImageSharp wrapper | Covered; package retained pending later functional review | 2026-09-16 | Leave ImageSharp 3.1.12 unchanged and revisit raster handling, licensing, and alternatives separately. | Five in-memory tests cover PNG and JPEG metadata, invalid and truncated input, SVG recognition, format-preserving encoding, proportional landscape and portrait resizing, no-upscale behavior, and invalid constraints. Later options are to license and upgrade to ImageSharp 4.1.2, retain a secure 3.x release, migrate to MIT-licensed SkiaSharp with its native assets and wrapper rewrite, or evaluate Magick.NET/ImageMagick for its broad format and processing support while accounting for native binaries and a heavier dependency footprint. This raster concern is distinct from BusinessMgr's Windows-only report printing. |
|
||||
| `SacsStandardBase` JSON utilities and `SysId` converter | Newtonsoft.Json integration | Covered for package upgrades | 2026-09-14 | Retain the suite for future Newtonsoft.Json updates. | Six tests cover objects, arrays, nulls, serializer settings, valid and invalid parsing paths, dynamic access, `SysId` conversion, independent deep copies, nested collections, and constructor-value replacement. |
|
||||
| `SacsStandardDbas` provider and parameter creation | Microsoft.Data.SqlClient, System.Data.Odbc, and registry integration | Providers aligned with .NET 10; broader coverage complete | 2026-09-15 | Add optional configured database integration tests when a suitable SQL Server or ODBC test database is available. | Microsoft.Data.SqlClient 7.0.3 and System.Data.Odbc 10.0.12 are covered by eleven database-free tests for provider selection, parsed SQL connection security and authentication settings, adapter creation, parameter order, scalar and null types, SQL `DateTime2`, ODBC `DateTime`, precision normalization, and sentinel dates. The suite explicitly verifies SqlClient 7's mandatory-encryption option. |
|
||||
| `SacsCoreUnitTests` | Test framework, SDK, and coverage tooling | Migrated to NUnit; coverage verified | 2026-09-15 | Use NUnit constraint assertions with `Assert.That` wherever possible. | Uses Microsoft.NET.Test.Sdk 18.10.0, NUnit 4.6.1, NUnit3TestAdapter 6.3.0, NUnit.Analyzers 4.15.0, and coverlet collector 10.0.1. All 126 tests use NUnit's constraint assertion model, pass under NUnit, and produce a non-empty Cobertura report in the optional validation coverage mode. |
|
||||
| `SacsCoreUnitTests` | Test framework, SDK, and coverage tooling | Migrated to NUnit; coverage verified | 2026-09-15 | Use NUnit constraint assertions with `Assert.That` wherever possible. | Uses Microsoft.NET.Test.Sdk 18.10.0, NUnit 4.6.1, NUnit3TestAdapter 6.3.0, NUnit.Analyzers 4.15.0, and coverlet collector 10.0.1. All 185 tests use NUnit's constraint assertion model, pass under NUnit, and produce a non-empty Cobertura report in the optional validation coverage mode. |
|
||||
| `SacsMobileXamarin` | Migration disposition | Isolated; retained for reference | 2026-09-10 | Keep outside the supported build unless a separately scoped resurrection or migration is approved. | Source is unchanged and available through `SaCommonVS.LegacyXamarin.sln`; see `docs/legacy-xamarin.md`. |
|
||||
| [`SacsMobileMaui`](../SacsMobileMaui/docs/component-reviews.md) | Project | .NET 10 migration complete; tracked in project docs | 2026-09-11 | Validate Apple targets on a Mac and continue API cleanup in the project tracker. | The shared project and both active mobile consumers target .NET 10. Windows runtime smoke tests pass, and Windows and Android Debug builds complete with zero errors. |
|
||||
| `SacsStandardBase/Base/Procs/SaProcess_Base.cs` | Class | In progress | 2026-09-09 | Complete the Process Base actions recorded in `topics.md`; review derived processes separately for cooperative polling. | Lifecycle state ownership and cancellation lock ordering were changed. Five focused tests pass. Remaining concerns include mixed sync/async execution, durable cancellation outcome, cancellation exception handling, disposal, and status synchronization. |
|
||||
| `SacsStandardBase/Base/Procs/SaProcess_Base.cs` | Class | Primary lifecycle protections complete; deferred semantics remain | 2026-09-17 | Scope disposal/status synchronization separately and review derived processes for cooperative polling. | One lifecycle guard rejects cross-mode and duplicate starts. `StopProcessAsync` cancels an async worker or requests manual interruption for an inline worker, then awaits its completion. A read-only interruption record retains the first manual, cancellation, or `OperationCanceledException` reason until the next run. Cancellation remains visible after asynchronous cleanup, including when cancellation occurs before the worker delegate starts, and `OperationCanceledException` is not reported as an error. Twelve focused tests pass; no production `RunAsync()` consumers were found in the active sibling workspaces. |
|
||||
| `SacsStandardBase/Base/Email/SaEmailConsole.cs` | Class | Dependency upgrade complete; security action still required | 2026-09-15 | Separately restore SMTP certificate validation and require the configured TLS mode. | MailKit 4.18.0 and its resolved MimeKit dependency pass the Common and downstream validation suites without vulnerability warnings. Message construction is covered for addressing, bodies, attachments, content IDs, default filenames, and tags. The current SMTP client still accepts every server certificate. |
|
||||
| `SacsStandardBase/Base/Helpers/SacsContentTypeHelper.cs` | Class | Framework dependency modernised | 2026-09-15 | Retain the content-type tests for future framework updates. | Removed direct references to the obsolete ASP.NET Core 2.3 `Http` and `StaticFiles` packages and now consumes their APIs from the .NET 10 `Microsoft.AspNetCore.App` shared framework. Ten regression cases cover filename lookup, generic form-file fallback, defaults, and bidirectional MIME mappings. |
|
||||
| `SacsStandardBase/Base/Net/SacsHttpClient.cs` | Class | Zstandard dependency upgraded and covered | 2026-09-15 | Retain the loopback response test for future HTTP and ZstdNet updates. | ZstdNet 1.5.7 replaces 1.4.5. An in-process HTTP test exercises the real response path with native Zstandard compression and verifies successful UTF-8 decompression without external network access. |
|
||||
| `SacsStandardProc/Proc/Excel/SaMsSpreadsheet_OpenXml.cs` | Class | Dependency upgrade complete; implementation remains incomplete | 2026-09-15 | Scope functional completion and dedicated wrapper tests separately if this implementation is to be revived. | DocumentFormat.OpenXml 3.5.1 builds and passes the existing spreadsheet and downstream validation. The Open XML implementation contains numerous pre-existing unsupported or placeholder operations, so this update does not present it as production-complete. |
|
||||
| `SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs` | Class | Dependency upgrade complete | 2026-09-12 | Retain the focused spreadsheet suite for future NPOI updates; do not adopt NPOI 2.8.0 without an explicit licensing decision. | NPOI 2.7.6 is paired with an explicit `System.Security.Cryptography.Xml` 10.0.12 reference and NuGet reports no vulnerable packages for `SacsStandardProc`. Nullable date and indexed-colour API changes were adapted. Ten focused tests and all 111 Common tests pass; BusinessMgr builds and its 21 tests pass. |
|
||||
| `SacsAuthenticatedEncryption`, `SacsEncryptionHelper`, and `SacsAesGcmCrypter` | Authenticated-encryption migration | Clean-break migration implemented | 2026-09-16 | Retain the focused suite; later move embedded application keys to protected configuration and add key rotation. | The `SACSENC2.` envelope uses AES-GCM with a fresh 12-byte nonce, a 16-byte tag, and purpose-bound associated data. Existing in-code keys are retained by decision, while CBC, fixed/session IVs, legacy reads, and plaintext fallback are removed. Thirty focused tests pass. See [`authenticated-encryption-migration.md`](authenticated-encryption-migration.md). |
|
||||
| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-16 | Use as the standard validation entry point and extend it when repository-wide checks change. | Builds serially, runs the 11-test focused SQL/ODBC-provider suite, the 10-test focused NPOI suite, and all 178 Common tests; audits both package-owning projects; verifies changed-file line endings and diffs; then invokes BusinessMgr validation. |
|
||||
| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-16 | Use as the standard validation entry point and extend it when repository-wide checks change. | Builds serially, runs the 11-test focused SQL/ODBC-provider suite, the 10-test focused NPOI suite, and all 185 Common tests; audits both package-owning projects; verifies changed-file line endings and diffs; then invokes BusinessMgr validation. |
|
||||
|
||||
+1
-5
@@ -15,10 +15,6 @@ Last updated: 2026-09-16
|
||||
|---|---|---|---|---|
|
||||
| P2 | Externalise and rotate encryption keys | 2026-09-16 | Move the retained in-code application keys to protected configuration or a secret store, then add key identifiers and a rotation policy in a separately scoped change. | The agreed clean-break AES-GCM migration is complete and intentionally retains the existing key strings. This follow-up is defense-in-depth and requires an operational key-deployment decision. |
|
||||
| P3 | Review raster image handling and ImageSharp | 2026-09-16 | Retain ImageSharp 3.1.12 for now and revisit the wrapper, licensing, and alternatives as a separately scoped functional review. | Version 3.1.12 has no reported vulnerability and five focused tests cover the current narrow use. Later options are: obtain an eligible free or commercial licence and upgrade to ImageSharp 4.1.2; retain a supported secure 3.x release; migrate to MIT-licensed SkiaSharp, accepting native runtime assets and a wrapper rewrite; or evaluate Magick.NET/ImageMagick as a candidate with broad image-format and processing support but native binaries and a heavier dependency footprint. This review is separate from BusinessMgr's Windows printer integration. |
|
||||
| P1 | Prevent synchronous and asynchronous execution from overlapping in `SaProcess_Base` | 2026-09-09 | Route `RunProcess()` and `RunAsync()` through one guarded start operation and extend the lifecycle tests. | A synchronous call can currently reuse and dispose the asynchronous worker's task data. |
|
||||
| P2 | Retain cancellation as a completed process outcome | 2026-09-09 | Store a durable cancellation result until the next process reset and test post-completion state. | `IsInterrupted` currently loses token-based cancellation after task-data cleanup. |
|
||||
| P2 | Handle `OperationCanceledException` as cancellation | 2026-09-09 | Catch cancellation separately in `RunProcess()` and avoid recording it as an error. | The general exception handler currently consumes cancellation before `RunAsync()` can distinguish it. |
|
||||
| P2 | Observe background `RunAsync()` tasks in consumers | 2026-09-09 | Review downstream fire-and-forget call sites and give their process consoles ownership of the returned task. | Rebuilt consumers are binary-compatible, but some source call sites still discard the task. |
|
||||
| P3 | Define disposal and cross-thread status semantics | 2026-09-09 | Decide whether to add asynchronous disposal and a synchronized process-status snapshot. | Disposal currently requests cancellation without awaiting termination; several status properties are read across threads. |
|
||||
|
||||
## Project-Specific Trackers
|
||||
@@ -39,12 +35,12 @@ Last updated: 2026-09-16
|
||||
|
||||
| Priority | Topic | Date noted | Date started | Next step | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| P1 | Review and harden `SaProcess_Base` lifecycle and cancellation | 2026-09-08 | 2026-09-08 | Address the prioritised Process Base findings above, beginning with prevention of mixed synchronous/asynchronous execution. | Task ownership, timeout retention, cancellation lock ordering, documentation, and initial lifecycle tests are complete. Cooperative polling in individual derived processes will be handled separately. |
|
||||
|
||||
## Completed Topics
|
||||
|
||||
| Topic | Date noted | Date started | Date completed | Outcome |
|
||||
|---|---|---|---|---|
|
||||
| Complete primary `SaProcess_Base` lifecycle protections | 2026-09-08 | 2026-09-08 | 2026-09-17 | A single lifecycle guard now rejects overlapping inline and asynchronous starts in either direction. A read-only interruption record preserves the first reason (manual request, cancellation request, or `OperationCanceledException`) until the next run resets it. `StopProcessAsync` now selects token cancellation for an asynchronous worker or a manual interruption request for an inline worker, then awaits completion. Cancellation is retained after asynchronous task-data cleanup, `OperationCanceledException` becomes cancellation rather than an error, and the async worker always runs its cleanup even if cancellation occurs before its delegate begins. Twelve focused tests cover successful execution, both overlap directions, pre-start cancellation, both stop modes, timeout retention, interruption reasons, cooperative cancellation, cancellation exceptions, and callback lock safety. No production `RunAsync()` call sites were found across Common, BusinessMgr, Sandtrap, or Sandalwood. Deferred work is limited to disposal/status semantics and derived-process cooperative polling. |
|
||||
| Replace legacy CBC encryption with authenticated encryption | 2026-09-08 | 2026-09-16 | 2026-09-16 | Implemented the `SACSENC2.` AES-GCM envelope with fresh nonces, 16-byte authentication tags, and purpose-bound associated data. Removed active CBC, fixed/session IVs, legacy reads, and plaintext fallback. Retained existing in-code keys by explicit decision. Thirty focused Common tests and BusinessMgr consumer tests cover the new behavior; see [`authenticated-encryption-migration.md`](authenticated-encryption-migration.md). |
|
||||
| Remove the obsolete PBKDF2 usage and establish encryption compatibility coverage | 2026-09-10 | 2026-09-16 | 2026-09-16 | Replaced the obsolete `Rfc2898DeriveBytes` constructor with static `Pbkdf2` using the same password, fixed salt, 300 iterations, SHA-256, and key length. Fixed legacy ciphertext proved derived-key and stored-data compatibility before the subsequent clean-break authenticated-encryption migration replaced that legacy format. Expanded direct coverage across both encryption helpers, corrected whitespace-only key acceptance, and disposed AES and transform resources. |
|
||||
| Harden and cover `AssemblyUtil` PE timestamp handling | 2026-09-10 | 2026-09-16 | 2026-09-16 | Replaced the unchecked partial reads and marshalled COFF structure with exact primitive reads, PE-signature validation, and graceful handling of missing, malformed, or truncated files. Added a public `TryGetBuildDateTime` extension while preserving `GetBuildDateTime`, plus five focused tests. The API documentation notes that deterministic-build COFF timestamps need not represent physical build time. |
|
||||
|
||||
Reference in New Issue
Block a user