Codex introduction. Base process tidy.
This commit is contained in:
+32
-1
@@ -1,8 +1,39 @@
|
||||
###############################################################################
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
# Normalize text in the Git index, with explicit working-tree endings below.
|
||||
###############################################################################
|
||||
* text=auto
|
||||
|
||||
# Windows and .NET source, project, configuration, and documentation files.
|
||||
*.cs text eol=crlf
|
||||
*.csproj text eol=crlf
|
||||
*.sln text eol=crlf
|
||||
*.props text eol=crlf
|
||||
*.targets text eol=crlf
|
||||
*.config text eol=crlf
|
||||
*.json text eol=crlf
|
||||
*.xml text eol=crlf
|
||||
*.md text eol=crlf
|
||||
*.txt text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Web assets and files commonly consumed by Unix tooling.
|
||||
*.svg text eol=lf
|
||||
*.html text eol=lf
|
||||
*.css text eol=lf
|
||||
*.js text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Binary assets.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
|
||||
###############################################################################
|
||||
# Set default behavior for command prompt diff.
|
||||
#
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
`SaCommonVS.sln` contains the shared Sarand libraries. `SacsStandardBase` provides core types and utilities; `SacsStandardDbas` provides database support; `SacsStandardProc` provides process and file-processing support; `SacsCoreWeb` provides ASP.NET components; and `SacsStandardAccounts` provides accounting integrations. Mobile support is in `SacsMobileMaui` and `SacsMobileXamarin`. Tests live in `SacsCoreUnitTests` and generally mirror the corresponding source area under `Standard`.
|
||||
|
||||
These projects are consumed by sibling repositories, including `..\..\BusinessMgr\Workspace`. Preserve the relative repository layout when validating changes against downstream solutions.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Run commands from the repository root:
|
||||
|
||||
```powershell
|
||||
dotnet restore SaCommonVS.sln
|
||||
dotnet build SaCommonVS.sln --configuration Debug
|
||||
dotnet test SacsCoreUnitTests/SacsCoreUnitTests.csproj --configuration Debug
|
||||
```
|
||||
|
||||
Use `--configuration Release` for release validation. Build the directly affected project first, then build the solution when a change affects public APIs or shared behavior. Mobile projects may require installed MAUI workloads and platform-specific tooling; report any unavailable workload separately from source failures.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Match the existing C# conventions: tabs, braces on new lines, top-of-file `using` directives, PascalCase public members, and established `Sacs`, `Sa`, `Sam`, and `Sax` prefixes. Follow nearby code where no repository-wide formatter rule exists. Target the framework versions declared in project files. Do not upgrade frameworks or NuGet packages unless explicitly requested.
|
||||
|
||||
Processes derived from `SaProcess_Base` must actively poll `IsInterrupted` during long-running loops and before expensive or blocking stages, then exit promptly when cancellation is requested.
|
||||
|
||||
Use Windows CRLF line endings for C# source, Visual Studio project files, configuration files, PowerShell or command scripts, and general documentation. Use LF for web assets and files commonly consumed by Unix tooling, including SVG, HTML, CSS, JavaScript, shell scripts, and YAML. Follow `.gitattributes` as the authoritative policy and preserve the configured ending when editing files.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Tests use MSTest and live in `SacsCoreUnitTests`. Add focused tests under the directory matching the affected source component. Run the affected tests after a change and run the full test project when behavior crosses module boundaries. Some database, network, or platform tests may require external configuration; distinguish environment failures from code regressions.
|
||||
|
||||
## Change Discipline
|
||||
|
||||
Do not edit generated files or build output under `bin`, `obj`, or `TestResults`. Change source inputs instead. Preserve public API compatibility because multiple sibling solutions consume these libraries. Before implementing a significant architectural or breaking change, describe the affected projects, compatibility implications, and migration approach and wait for agreement.
|
||||
|
||||
## Conversation Tracking
|
||||
|
||||
Read `docs/topics.md` and `docs/component-reviews.md` before starting repository review or follow-up work. Update them when a conversation identifies, starts, completes, or reprioritises work, or when a component is reviewed. Follow the tracking conventions in `docs/README.md` and use ISO `YYYY-MM-DD` dates.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Keep commits focused and use a concise imperative or clear feature/fix summary. Pull requests should describe the behavior changed, affected projects, compatibility implications, and build/test results. Note any required configuration, database, framework, or workload changes.
|
||||
|
||||
## Security & Configuration
|
||||
|
||||
Do not commit credentials, connection strings, tokens, certificates, or machine-specific secrets. Keep environment-specific configuration local. Treat changes to encryption, authentication, network certificate validation, serialization, and database query construction as security-sensitive and include focused validation.
|
||||
@@ -0,0 +1,170 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Sarand.Net.Standard.Base.Procs;
|
||||
using Sarand.Net.Standard.Base.Tasks;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sacs.UnitTests.Net.Standard.Base.Procs
|
||||
{
|
||||
//=======================================================================
|
||||
[TestClass]
|
||||
public class SaProcess_Base_Tester
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
private class TestProcessConfig : SaProcess_Base.Config
|
||||
{
|
||||
public TestProcessConfig()
|
||||
{
|
||||
AllowInterruption = true;
|
||||
ConfirmInterruption = false;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
private class TestProcess : SaProcess_Base
|
||||
{
|
||||
public TestProcess(bool stopOnCancellation) : base(new TestProcessConfig())
|
||||
{
|
||||
StopOnCancellation = stopOnCancellation;
|
||||
}
|
||||
|
||||
public override String ProcessDescription { get { return "Test process"; } }
|
||||
public ManualResetEventSlim Started { get; } = new ManualResetEventSlim(false);
|
||||
public ManualResetEventSlim Release { get; } = new ManualResetEventSlim(false);
|
||||
public bool CancellationObserved { get; private set; }
|
||||
private bool StopOnCancellation { get; }
|
||||
|
||||
protected override bool Run()
|
||||
{
|
||||
Started.Set();
|
||||
while (!Release.Wait(5))
|
||||
{
|
||||
if (StopOnCancellation && IsInterrupted)
|
||||
{
|
||||
CancellationObserved = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
[TestMethod]
|
||||
public async Task RunAsync_CompletesAndClearsAsyncState()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.IsTrue(process.Started.Wait(1000), "Process did not start.");
|
||||
Assert.IsTrue(process.IsAsync);
|
||||
|
||||
process.Release.Set();
|
||||
await runTask;
|
||||
|
||||
Assert.IsFalse(process.IsAsync);
|
||||
Assert.IsFalse(process.Processing);
|
||||
Assert.IsTrue(process.ProcessEnded);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
[TestMethod]
|
||||
public async Task RunAsync_RejectsOverlappingRun()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.IsTrue(process.Started.Wait(1000), "Process did not start.");
|
||||
|
||||
await Assert.ThrowsExceptionAsync<SaProcess_Base.InProgressException>(() => process.RunAsync());
|
||||
|
||||
process.Release.Set();
|
||||
await runTask;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
[TestMethod]
|
||||
public async Task StopProcessAsync_TimeoutRetainsAsyncState()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(false))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.IsTrue(process.Started.Wait(1000), "Process did not start.");
|
||||
|
||||
Assert.IsFalse(await process.StopProcessAsync(40));
|
||||
Assert.IsTrue(process.IsAsync, "A timed-out stop must retain the active process state.");
|
||||
await Assert.ThrowsExceptionAsync<SaProcess_Base.InProgressException>(() => process.RunAsync());
|
||||
|
||||
process.Release.Set();
|
||||
await runTask;
|
||||
Assert.IsFalse(process.IsAsync);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
[TestMethod]
|
||||
public async Task StopProcessAsync_CooperativeCancellationStopsProcess()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(true))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.IsTrue(process.Started.Wait(1000), "Process did not start.");
|
||||
|
||||
Assert.IsTrue(await process.StopProcessAsync(1000));
|
||||
await runTask;
|
||||
|
||||
Assert.IsTrue(process.CancellationObserved);
|
||||
Assert.IsFalse(process.IsAsync);
|
||||
Assert.IsTrue(await process.StopProcessAsync(20), "Stopping an idle process should succeed.");
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
[TestMethod]
|
||||
public async Task StopProcessAsync_CancellationCallbackCanReadAsyncState()
|
||||
{
|
||||
using (TestProcess process = new TestProcess(true))
|
||||
{
|
||||
Task runTask = process.RunAsync();
|
||||
Assert.IsTrue(process.Started.Wait(1000), "Process did not start.");
|
||||
|
||||
CancellationToken cancellationToken = GetCancellationToken(process);
|
||||
bool callbackInvoked = false;
|
||||
using (cancellationToken.Register(() =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
Assert.IsTrue(process.IsAsync);
|
||||
}))
|
||||
{
|
||||
Task<bool> stopTask = process.StopProcessAsync(1000);
|
||||
Task completedTask = await Task.WhenAny(stopTask, Task.Delay(2000));
|
||||
Assert.AreSame(stopTask, completedTask, "Cancellation callback deadlocked on the lifecycle lock.");
|
||||
Assert.IsTrue(await stopTask);
|
||||
}
|
||||
|
||||
await runTask;
|
||||
Assert.IsTrue(callbackInvoked);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
private static CancellationToken GetCancellationToken(SaProcess_Base process)
|
||||
{
|
||||
PropertyInfo controlProperty = typeof(SaProcess_Base).GetProperty("Control", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
Assert.IsNotNull(controlProperty);
|
||||
Object control = controlProperty.GetValue(process);
|
||||
Assert.IsNotNull(control);
|
||||
|
||||
PropertyInfo taskDataProperty = control.GetType().GetProperty("TaskData", BindingFlags.Instance | BindingFlags.Public);
|
||||
Assert.IsNotNull(taskDataProperty);
|
||||
SacsTaskData taskData = (SacsTaskData)taskDataProperty.GetValue(control);
|
||||
Assert.IsNotNull(taskData);
|
||||
return taskData.CancellationToken;
|
||||
}
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# SacsMobileMaui Conversation Documentation
|
||||
|
||||
This folder records review findings and follow-up work specific to the `SacsMobileMaui` project and its mobile-app consumers.
|
||||
|
||||
## Files
|
||||
|
||||
- `topics.md` tracks prioritised, in-progress, and completed project work.
|
||||
- `component-reviews.md` records reviewed project paths and classes.
|
||||
|
||||
The Common workspace documentation in `../../docs` remains the index for shared and cross-project work.
|
||||
|
||||
## Tracking Rules
|
||||
|
||||
- Read both project trackers and the shared trackers before continuing a related review.
|
||||
- Use ISO `YYYY-MM-DD` dates.
|
||||
- Assign priorities as P1 (urgent or high risk), P2 (important), or P3 (improvement).
|
||||
- Keep an active topic in one state section. Move its row when work starts or completes rather than copying it between sections.
|
||||
- Preserve the original date noted when moving a topic. Add the dates started and completed as work progresses.
|
||||
- Record concrete next steps and validation evidence. Link to project files using paths relative to `SacsMobileMaui`.
|
||||
- Update the component register after a review, even when no action is required.
|
||||
@@ -0,0 +1,7 @@
|
||||
# SacsMobileMaui Component Review Register
|
||||
|
||||
Last updated: 2026-09-10
|
||||
|
||||
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `SacsMobileMaui` | Project | Reviewed; action required | 2026-09-10 | Address the MAUI 10 migration and focused defects recorded in `topics.md`, then build Windows/Android and validate Apple targets on a Mac. | The project targets unsupported MAUI 9.0.120 while its Sandalwood and Sandtrap consumers still target MAUI 8. Review found compatibility API debt, UI-thread synchronization defects, incorrect handler casts, an `AllowInvalid` setter bug, and discarded resource streams. Local validation was limited by an inconsistent installed workload manifest and write-protected existing `obj` output. |
|
||||
@@ -0,0 +1,23 @@
|
||||
# SacsMobileMaui Topic Tracker
|
||||
|
||||
Last updated: 2026-09-10
|
||||
|
||||
## Prioritised Topics to Be Actioned
|
||||
|
||||
| Priority | Topic | Date noted | Next step | Notes |
|
||||
|---|---|---|---|---|
|
||||
| P1 | Upgrade `SacsMobileMaui` and its consumers from unsupported .NET MAUI 8/9 to .NET MAUI 10 | 2026-09-10 | Replace MAUI 9 compatibility APIs, align and validate consumer projects, then retarget to `net10.0` and the current MAUI 10 servicing release. | MAUI 9.0.120 is the final MAUI 9 patch, but MAUI 9 support ended on 2026-05-12. Sandalwood and Sandtrap still target MAUI 8 while referencing the `net9.0` shared project. The shared library also uses removed/deprecated compatibility layouts, `Frame`, `MainPage`, and popup APIs. |
|
||||
| P1 | Make `SamThreadUtil` and form dialogs genuinely asynchronous and deadlock-safe | 2026-09-10 | Replace synchronous UI-thread bridges with `Task`-returning APIs, update the Sandtrap caller, and add focused tests. | The void `InvokeOnMainThreadSync` overload never waits; the generic overload and `DisplayConfirmSync` block on `Task.Result` and can deadlock on the UI thread. `DisplayAlertSync` also passes an async lambda to `Action`, producing `async void` behavior. |
|
||||
| P2 | Correct MAUI native handler type checks | 2026-09-10 | Use `SamEditor` in editor mappings and `SamPicker` in picker mappings; validate each platform-specific option. | Several editor and every picker option currently cast to the wrong custom control type, so configured native styling is silently skipped. |
|
||||
| P2 | Fix `SamDatePicker.AllowInvalid` state transition | 2026-09-10 | Change the assignment in the equality guard, verify both transitions, and add tests for display and reset behavior. | Setting `AllowInvalid` to `true` returns before refreshing the control; the guard uses `allowInvalidDate = value`. |
|
||||
| P3 | Remove unused resource-stream enumeration | 2026-09-10 | Return `null` immediately after a failed named lookup or implement an intentional lookup strategy that disposes rejected streams. | `GetResourceStream` opens every manifest resource, discards each stream, and returns `null`. |
|
||||
|
||||
## In-Progress Topics
|
||||
|
||||
| Priority | Topic | Date noted | Date started | Next step | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## Completed Topics
|
||||
|
||||
| Topic | Date noted | Date started | Date completed | Outcome |
|
||||
|---|---|---|---|---|
|
||||
@@ -1,4 +1,4 @@
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.Core;
|
||||
using Sarand.Net.Standard.Base.Tasks;
|
||||
using Sarand.Net.Standard.Base.Utils;
|
||||
@@ -178,6 +178,12 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Task control data.
|
||||
private SacsTaskData taskData = null;
|
||||
public SacsTaskData TaskData { get { return taskData; } set { if (taskData != null) taskData.Dispose(); taskData = value; } }
|
||||
public bool DetachTaskData(SacsTaskData expected)
|
||||
{
|
||||
if (!Object.ReferenceEquals(taskData, expected)) return false;
|
||||
taskData = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -195,8 +201,15 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Clear process data to dispose.
|
||||
Control.TaskData = null;
|
||||
AsyncProcessData asyncData;
|
||||
lock (AsyncLock)
|
||||
{
|
||||
asyncData = AsyncData;
|
||||
if (asyncData == null)
|
||||
Control.TaskData = null;
|
||||
}
|
||||
// A running worker owns its task data until it exits.
|
||||
asyncData?.RequestCancellation();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,65 +243,96 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Run process asynchronously.
|
||||
private class AsyncProcessData : SacsDisposable
|
||||
{
|
||||
public AsyncProcessData(Task task)
|
||||
private readonly Object CancellationLock = new object();
|
||||
|
||||
public AsyncProcessData(SacsTaskData taskData)
|
||||
{
|
||||
this.Task = task;
|
||||
this.TaskData = taskData;
|
||||
}
|
||||
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)
|
||||
if (disposing)
|
||||
{
|
||||
AsyncData.Dispose();
|
||||
AsyncData = null;
|
||||
lock (CancellationLock)
|
||||
TaskData.Dispose();
|
||||
}
|
||||
}
|
||||
public Task Task { get; set; }
|
||||
public SacsTaskData TaskData { get; }
|
||||
public bool RequestCancellation()
|
||||
{
|
||||
lock (CancellationLock)
|
||||
return TaskData.TriggerCancellation();
|
||||
}
|
||||
}
|
||||
private readonly Object AsyncLock = new object();
|
||||
private AsyncProcessData AsyncData { get; set; }
|
||||
public bool IsAsync { get { lock (AsyncLock) { return AsyncData != null; } } }
|
||||
private void ClearAsyncData(AsyncProcessData asyncData)
|
||||
{
|
||||
bool clear = false;
|
||||
lock (AsyncLock)
|
||||
{
|
||||
// Only the task that owns the current state may clear it.
|
||||
if (Object.ReferenceEquals(AsyncData, asyncData))
|
||||
{
|
||||
Control.DetachTaskData(asyncData.TaskData);
|
||||
AsyncData = null;
|
||||
clear = true;
|
||||
}
|
||||
}
|
||||
// Dispose outside the lifecycle lock. Disposal waits for any cancellation callback.
|
||||
if (clear)
|
||||
asyncData.Dispose();
|
||||
}
|
||||
/// <summary>
|
||||
/// Run the process asynchronously.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InProgressException">If process is already running.</exception>
|
||||
public async void RunAsync()
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (AsyncData != null)
|
||||
throw new InProgressException();
|
||||
Control.TaskData = new SacsTaskData();
|
||||
// Run on separate thread.
|
||||
Task task = new Task(() =>
|
||||
AsyncProcessData asyncData;
|
||||
lock (AsyncLock)
|
||||
{
|
||||
try
|
||||
if (AsyncData != null)
|
||||
throw new InProgressException();
|
||||
|
||||
SacsTaskData taskData = new SacsTaskData();
|
||||
Control.TaskData = taskData;
|
||||
// Construct before starting so the process state is visible atomically.
|
||||
asyncData = new AsyncProcessData(taskData);
|
||||
Task task = new Task(() =>
|
||||
{
|
||||
// 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;
|
||||
try
|
||||
{
|
||||
// Execute the process.
|
||||
RunProcess(false);
|
||||
}
|
||||
catch (OperationCanceledException exc)
|
||||
{
|
||||
SaLogger.Warn(exc.GetType().Name + " during asynchronous processing on " + GetType().Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The worker owns its state until it has actually terminated.
|
||||
ClearAsyncData(asyncData);
|
||||
}
|
||||
}, taskData.CancellationToken);
|
||||
asyncData.Task = task;
|
||||
AsyncData = asyncData;
|
||||
task.Start();
|
||||
}
|
||||
await asyncData.Task;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Run process inline.
|
||||
public bool RunProcess()
|
||||
{
|
||||
return RunProcess(true);
|
||||
}
|
||||
private bool RunProcess(bool clearTaskData)
|
||||
{
|
||||
// Prepare process.
|
||||
bool runProcess = false;
|
||||
@@ -329,7 +373,8 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
FinishedAt = DateTime.Now;
|
||||
Processing = false;
|
||||
ProcessEnded = true;
|
||||
Control.TaskData = null;
|
||||
if (clearTaskData)
|
||||
Control.TaskData = null;
|
||||
}
|
||||
|
||||
// Process completed.
|
||||
@@ -349,28 +394,25 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
protected abstract bool Run();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
public async Task StopProcessAsync(int timeout = 50)
|
||||
/// <returns>True if no process is running or the process stopped within the timeout.</returns>
|
||||
public async Task<bool> StopProcessAsync(int timeout = 50)
|
||||
{
|
||||
if (!Processing) return;
|
||||
if (AsyncData != null)
|
||||
AsyncProcessData asyncData;
|
||||
lock (AsyncLock)
|
||||
asyncData = AsyncData;
|
||||
if (asyncData == 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)
|
||||
{
|
||||
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();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
await asyncData.Task;
|
||||
return true;
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -499,7 +541,7 @@ namespace Sarand.Net.Standard.Base.Procs
|
||||
// Interruption.
|
||||
public bool IsInterrupted
|
||||
{
|
||||
get { return Control.Interrupted; }
|
||||
get { return Control.Interrupted || (Control.TaskData?.IsCancellationRequested ?? false); }
|
||||
}
|
||||
public bool Interrupt()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Conversation Documentation
|
||||
|
||||
This folder records review findings and follow-up work relevant to the Common workspace.
|
||||
|
||||
## Files
|
||||
|
||||
- `topics.md` tracks prioritised work, work in progress, and completed work.
|
||||
- `component-reviews.md` records which projects, paths, and classes have been reviewed.
|
||||
|
||||
## Project Trackers
|
||||
|
||||
- `../SacsMobileMaui/docs/` contains topics and component reviews specific to the shared MAUI project and its consumers.
|
||||
|
||||
## Tracking Rules
|
||||
|
||||
- Read both shared trackers and any relevant project trackers before continuing an existing review or starting related work.
|
||||
- Use ISO `YYYY-MM-DD` dates.
|
||||
- Assign priorities as P1 (urgent or high risk), P2 (important), or P3 (improvement).
|
||||
- Keep an active topic in one state section. Move its row when work starts or completes rather than copying the active topic between state sections.
|
||||
- Preserve the original date noted when moving a topic. Add the date started and date completed as the topic progresses.
|
||||
- Record concrete next steps and validation evidence. Link to workspace files using repository-relative paths.
|
||||
- Update the component register after a review, even when no action is required.
|
||||
@@ -0,0 +1,10 @@
|
||||
# Component Review Register
|
||||
|
||||
Last updated: 2026-09-10
|
||||
|
||||
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [`SacsMobileMaui`](../SacsMobileMaui/docs/component-reviews.md) | Project | Tracked in project docs | 2026-09-10 | Continue in the project component register and topic tracker. | Project-specific findings have been moved to `SacsMobileMaui/docs`. |
|
||||
| `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/Email/SaEmailConsole.cs` | Class | Reviewed; action required | 2026-09-08 | Restore SMTP certificate validation and require the configured TLS mode. Add invalid-certificate and unavailable-TLS tests. | The current SMTP client accepts every server certificate. |
|
||||
| `SacsStandardBase/Base/Helpers/SacsEncryptionHelper.cs` and `SacsStandardBase/Base/Data/SacsAesIvCrypter.cs` | Classes | Reviewed; action required | 2026-09-08 | Design a versioned authenticated-encryption format with legacy migration and tamper tests. | Current CBC encryption is unauthenticated; password-based key derivation uses a fixed salt and low iteration count. |
|
||||
@@ -0,0 +1,31 @@
|
||||
# Topic Tracker
|
||||
|
||||
Last updated: 2026-09-10
|
||||
|
||||
## Prioritised Topics to Be Actioned
|
||||
|
||||
| Priority | Topic | Date noted | Next step | Notes |
|
||||
|---|---|---|---|---|
|
||||
| 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
|
||||
|
||||
- [`SacsMobileMaui`](../SacsMobileMaui/docs/topics.md) tracks the MAUI framework upgrade, UI-thread helpers, native handlers, controls, and resources.
|
||||
|
||||
## In-Progress Topics
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|
|
||||
| Make asynchronous Process Base execution awaitable and retain state after a stop timeout | 2026-09-08 | 2026-09-08 | 2026-09-08 | `RunAsync()` returns `Task`; `StopProcessAsync()` reports success and leaves active state owned by a worker that has not stopped. |
|
||||
| Remove cancellation callbacks from the Process Base lifecycle lock | 2026-09-08 | 2026-09-08 | 2026-09-08 | Cancellation and task-data disposal use separate synchronization; worker cleanup detaches state before disposal. |
|
||||
| Add Process Base lifecycle tests | 2026-09-08 | 2026-09-08 | 2026-09-08 | Five focused MSTest cases cover completion, duplicate starts, stop timeout retention, cooperative cancellation, and cancellation callback lock behavior. |
|
||||
Reference in New Issue
Block a user