//
// NosThreadSynchronizer.cs
//
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Options;
using Remora.Results;
namespace NosSmooth.LocalBinding;
///
/// Synchronizes with NosTale thread using a periodic function.
///
public class NosThreadSynchronizer
{
private readonly IPeriodicHook _periodicHook;
private readonly ILogger _logger;
private readonly NosThreadSynchronizerOptions _options;
private readonly ConcurrentQueue _queuedOperations;
private Thread? _nostaleThread;
///
/// Initializes a new instance of the class.
///
/// The periodic hook.
/// The logger.
/// The options.
public NosThreadSynchronizer
(
IPeriodicHook periodicHook,
ILogger logger,
IOptions options
)
{
_periodicHook = periodicHook;
_logger = logger;
_options = options.Value;
_queuedOperations = new ConcurrentQueue();
}
///
/// Gets whether the current thread is a NosTale thread.
///
public bool IsSynchronized => _nostaleThread == Thread.CurrentThread;
///
/// Start the synchronizer operation.
///
public void StartSynchronizer()
{
_periodicHook.Called += PeriodicCall;
}
///
/// Stop the synchronizer operation.
///
public void StopSynchronizer()
{
_periodicHook.Called -= PeriodicCall;
}
private void PeriodicCall(object? owner, System.EventArgs eventArgs)
{
_nostaleThread = Thread.CurrentThread;
var tasks = _options.MaxTasksPerIteration;
while (tasks-- > 0 && _queuedOperations.TryDequeue(out var operation))
{
ExecuteOperation(operation);
}
}
private void ExecuteOperation(SyncOperation operation)
{
try
{
var result = operation.Action();
operation.Result = result;
}
catch (Exception e)
{
_logger.LogError(e, "Synchronizer obtained an exception");
operation.Result = (Result)e;
}
if (operation.CancellationTokenSource is not null)
{
try
{
operation.CancellationTokenSource.Cancel();
}
catch (Exception)
{
// ignore
}
}
}
///
/// Enqueue the given operation to execute on next frame.
///
/// The action to execute.
/// Whether to execute the operation instantly in case we are on the NosTale thread.
public void EnqueueOperation(Action action, bool executeIfSynchronized = true)
{
if (executeIfSynchronized && IsSynchronized)
{ // we are synchronized, no need to wait.
action();
return;
}
_queuedOperations.Enqueue
(
new SyncOperation
(
() =>
{
action();
return Result.FromSuccess();
},
null
)
);
}
///
/// Synchronizes to NosTale thread, executes the given action and returns its result.
///
/// The action to execute.
/// The cancellation token used for cancelling the operation.
/// The result of the action.
public async Task SynchronizeAsync(Func action, CancellationToken ct = default)
{
return (Result)await CommonSynchronizeAsync(() => action(), ct);
}
///
/// Synchronizes to NosTale thread, executes the given action and returns its result.
///
/// The action to execute.
/// The cancellation token used for cancelling the operation.
/// The result of the action.
/// The type of the result.
public async Task> SynchronizeAsync(Func> action, CancellationToken ct = default)
{
return (Result)await CommonSynchronizeAsync(() => action(), ct);
}
private async Task CommonSynchronizeAsync(Func action, CancellationToken ct = default)
{
if (IsSynchronized)
{ // we are already synchronized, execute the action.
try
{
return action();
}
catch (Exception e)
{
return (Result)e;
}
}
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
var syncOperation = new SyncOperation(action, linkedSource);
_queuedOperations.Enqueue(syncOperation);
try
{
await Task.Delay(Timeout.Infinite, linkedSource.Token);
}
catch (OperationCanceledException)
{
if (ct.IsCancellationRequested)
{ // Throw in case the top token was cancelled.
throw;
}
}
catch (Exception e)
{
return (Result)new ExceptionError(e);
}
return syncOperation.Result ?? Result.FromSuccess();
}
private record SyncOperation(Func Action, CancellationTokenSource? CancellationTokenSource)
{
public IResult? Result { get; set; }
}
}